diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/cloud-workspaces/components/CloudWorkspacesHeader/CloudWorkspacesHeader.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/cloud-workspaces/components/CloudWorkspacesHeader/CloudWorkspacesHeader.tsx index 1b32b0bd9d7..66c0439ac90 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/cloud-workspaces/components/CloudWorkspacesHeader/CloudWorkspacesHeader.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/cloud-workspaces/components/CloudWorkspacesHeader/CloudWorkspacesHeader.tsx @@ -1,5 +1,6 @@ import { plural } from "@lingui/core/macro"; import { Trans, useLingui } from "@lingui/react/macro"; +import type { CloudWorkspaceSort } from "@superset/shared/cloud-workspace-groups"; import { AvatarStack } from "@superset/ui/atoms/AvatarStack"; import { DropdownMenu, @@ -33,7 +34,6 @@ import { import { ToolbarMenuButton } from "renderer/routes/_authenticated/_dashboard/components/ToolbarMenuButton"; import { WorkItemsSearch } from "renderer/routes/_authenticated/_dashboard/components/WorkItemsSearch"; import type { CloudWorkspaceGrouping } from "renderer/routes/_authenticated/_dashboard/stores/listDisplayStore"; -import type { CloudWorkspaceSort } from "renderer/routes/_authenticated/_dashboard/utils/groupCloudWorkspaces"; import { NO_PROJECT } from "../../constants"; import type { CloudWorkspaceStatusFilter } from "../../types"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/cloud-workspaces/components/CloudWorkspacesView/CloudWorkspacesView.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/cloud-workspaces/components/CloudWorkspacesView/CloudWorkspacesView.tsx index 82fd6ba9ade..e2ca28f0712 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/cloud-workspaces/components/CloudWorkspacesView/CloudWorkspacesView.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/cloud-workspaces/components/CloudWorkspacesView/CloudWorkspacesView.tsx @@ -1,4 +1,9 @@ -import { getActiveLocale } from "@superset/i18n/format"; +import { formatRelativePeriod } from "@superset/i18n/format"; +import { + groupCloudWorkspaces, + groupCloudWorkspacesByTime, + sortCloudWorkspaces, +} from "@superset/shared/cloud-workspace-groups"; import { useNavigate } from "@tanstack/react-router"; import { useMemo, useState } from "react"; import { useActiveOrganizationId } from "renderer/hooks/useActiveOrganizationId"; @@ -11,26 +16,12 @@ import { CloudWorkspacesList } from "renderer/routes/_authenticated/_dashboard/c import { useCloudWorkspaceListItems } from "renderer/routes/_authenticated/_dashboard/hooks/useCloudWorkspaceListItems"; import { useUnarchiveCloudWorkspace } from "renderer/routes/_authenticated/_dashboard/hooks/useUnarchiveCloudWorkspace"; import { useListDisplayStore } from "renderer/routes/_authenticated/_dashboard/stores/listDisplayStore"; -import { - type CloudWorkspacePeriod, - groupCloudWorkspaces, - groupCloudWorkspacesByTime, - sortCloudWorkspaces, -} from "renderer/routes/_authenticated/_dashboard/utils/groupCloudWorkspaces"; import { NO_PROJECT } from "../../constants"; import type { CloudWorkspacesSearch } from "../../types"; import { CloudWorkspacesHeader } from "../CloudWorkspacesHeader"; const NOW_TICK_MS = 30_000; -function formatPeriod({ unit, count }: CloudWorkspacePeriod) { - const locale = getActiveLocale(); - const label = new Intl.RelativeTimeFormat(locale, { - numeric: "auto", - }).format(-count, unit); - return label.charAt(0).toLocaleUpperCase(locale) + label.slice(1); -} - interface CloudWorkspacesViewProps { search: CloudWorkspacesSearch; } @@ -172,7 +163,7 @@ export function CloudWorkspacesView({ search }: CloudWorkspacesViewProps) { sort: display.sort, }).map(({ period, workspaces: grouped }) => ({ key: `${period.unit}:${period.count}`, - label: formatPeriod(period), + label: formatRelativePeriod(period), items: grouped.map(listItems.toItem), })), } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/projects/$projectId/components/ProjectRecordScreen/ProjectRecordScreen.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/projects/$projectId/components/ProjectRecordScreen/ProjectRecordScreen.tsx index 6597564af54..cbdbcb5d979 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/projects/$projectId/components/ProjectRecordScreen/ProjectRecordScreen.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/projects/$projectId/components/ProjectRecordScreen/ProjectRecordScreen.tsx @@ -1,5 +1,6 @@ import { useLingui } from "@lingui/react/macro"; import { errorMessage } from "@superset/i18n/errors"; +import { sortCloudWorkspaces } from "@superset/shared/cloud-workspace-groups"; import { toast } from "@superset/ui/sonner"; import { useNavigate } from "@tanstack/react-router"; import { useRef } from "react"; @@ -9,7 +10,6 @@ import { cloudTrpc } from "renderer/lib/cloud-trpc"; import { StateScreenShell } from "renderer/routes/_authenticated/_dashboard/components/StateScreenShell"; import { useCloudWorkspaceListItems } from "renderer/routes/_authenticated/_dashboard/hooks/useCloudWorkspaceListItems"; import { useOrganizationPeople } from "renderer/routes/_authenticated/_dashboard/hooks/useOrganizationPeople"; -import { sortCloudWorkspaces } from "renderer/routes/_authenticated/_dashboard/utils/groupCloudWorkspaces"; import { useInviteMember } from "renderer/routes/_authenticated/hooks/useInviteMember"; import type { ProjectRecord, ProjectTab } from "../../types"; import { ProjectRecordView } from "../ProjectRecordView"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/stores/listDisplayStore/listDisplayStore.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/stores/listDisplayStore/listDisplayStore.ts index 9aff18cc5c2..183471cb6ab 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/stores/listDisplayStore/listDisplayStore.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/stores/listDisplayStore/listDisplayStore.ts @@ -1,4 +1,4 @@ -import type { CloudWorkspaceSort } from "renderer/routes/_authenticated/_dashboard/utils/groupCloudWorkspaces"; +import type { CloudWorkspaceSort } from "@superset/shared/cloud-workspace-groups"; import { create } from "zustand"; import { persist } from "zustand/middleware"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/utils/buildCloudSidebar/buildCloudSidebar.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/utils/buildCloudSidebar/buildCloudSidebar.ts index 1d5d6b82d99..fd807db7dc1 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/utils/buildCloudSidebar/buildCloudSidebar.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/utils/buildCloudSidebar/buildCloudSidebar.ts @@ -1,3 +1,4 @@ +import { sortByLastAgentMessage } from "@superset/shared/cloud-workspace-groups"; import type { CloudWorkspaceRow } from "renderer/hooks/useCloudWorkspaces"; import type { CloudSidebarEntry, @@ -10,18 +11,6 @@ type SidebarWorkspace = Pick< "id" | "createdAt" | "createdBy" | "agentStatusAt" >; -const lastAgentMessageAt = ( - workspace: Pick, -) => (workspace.agentStatusAt ?? workspace.createdAt).getTime(); - -export function sortByLastAgentMessage< - Workspace extends Pick, ->(workspaces: Workspace[]): Workspace[] { - return [...workspaces].sort( - (left, right) => lastAgentMessageAt(right) - lastAgentMessageAt(left), - ); -} - export function isInCloudSidebar( workspace: Pick, entry: CloudSidebarEntry | undefined, diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/utils/buildCloudSidebar/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/utils/buildCloudSidebar/index.ts index 31a236552e8..b264e83be1d 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/utils/buildCloudSidebar/index.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/utils/buildCloudSidebar/index.ts @@ -3,5 +3,4 @@ export { type CloudSidebarLayout, isCloudWorkspaceRead, isInCloudSidebar, - sortByLastAgentMessage, } from "./buildCloudSidebar"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/utils/groupCloudWorkspaces/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/utils/groupCloudWorkspaces/index.ts deleted file mode 100644 index 5af7b2ebdd6..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/utils/groupCloudWorkspaces/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { - type CloudWorkspacePeriod, - type CloudWorkspaceSort, - groupCloudWorkspaces, - groupCloudWorkspacesByTime, - sortCloudWorkspaces, -} from "./groupCloudWorkspaces"; diff --git a/apps/mobile/app/(authenticated)/(home)/filter/_layout.tsx b/apps/mobile/app/(authenticated)/(home)/filter/_layout.tsx index 4a76429d2a9..ee93e3f702d 100644 --- a/apps/mobile/app/(authenticated)/(home)/filter/_layout.tsx +++ b/apps/mobile/app/(authenticated)/(home)/filter/_layout.tsx @@ -24,6 +24,14 @@ export default function FilterLayout() { }} /> + + ); } diff --git a/apps/mobile/app/(authenticated)/(home)/filter/creator.tsx b/apps/mobile/app/(authenticated)/(home)/filter/creator.tsx new file mode 100644 index 00000000000..41505587190 --- /dev/null +++ b/apps/mobile/app/(authenticated)/(home)/filter/creator.tsx @@ -0,0 +1,3 @@ +import { CreatorFilterScreen } from "@/screens/(authenticated)/(home)/filter/creator"; + +export default CreatorFilterScreen; diff --git a/apps/mobile/app/(authenticated)/(home)/filter/status.tsx b/apps/mobile/app/(authenticated)/(home)/filter/status.tsx new file mode 100644 index 00000000000..b9140d23989 --- /dev/null +++ b/apps/mobile/app/(authenticated)/(home)/filter/status.tsx @@ -0,0 +1,3 @@ +import { StatusFilterScreen } from "@/screens/(authenticated)/(home)/filter/status"; + +export default StatusFilterScreen; diff --git a/apps/mobile/app/(authenticated)/workspace/[id]/index.tsx b/apps/mobile/app/(authenticated)/workspace/[id]/index.tsx index 07245a65e7d..83c971935d3 100644 --- a/apps/mobile/app/(authenticated)/workspace/[id]/index.tsx +++ b/apps/mobile/app/(authenticated)/workspace/[id]/index.tsx @@ -1,3 +1,7 @@ +import { useLocalSearchParams } from "expo-router"; import { WorkspaceScreen } from "@/screens/(authenticated)/workspace/[id]/WorkspaceScreen"; -export default WorkspaceScreen; +export default function WorkspaceRoute() { + const { id } = useLocalSearchParams<{ id: string }>(); + return ; +} diff --git a/apps/mobile/hooks/useArchivedCloudWorkspaces/index.ts b/apps/mobile/hooks/useArchivedCloudWorkspaces/index.ts new file mode 100644 index 00000000000..c456ef9585a --- /dev/null +++ b/apps/mobile/hooks/useArchivedCloudWorkspaces/index.ts @@ -0,0 +1,4 @@ +export { + getArchivedCloudWorkspacesQueryKey, + useArchivedCloudWorkspaces, +} from "./useArchivedCloudWorkspaces"; diff --git a/apps/mobile/hooks/useArchivedCloudWorkspaces/useArchivedCloudWorkspaces.ts b/apps/mobile/hooks/useArchivedCloudWorkspaces/useArchivedCloudWorkspaces.ts new file mode 100644 index 00000000000..0eb550ef57a --- /dev/null +++ b/apps/mobile/hooks/useArchivedCloudWorkspaces/useArchivedCloudWorkspaces.ts @@ -0,0 +1,76 @@ +import { FEATURE_FLAGS } from "@superset/shared/constants"; +import { useQuery } from "@tanstack/react-query"; +import { TRPCClientError } from "@trpc/client"; +import { useFeatureFlag } from "posthog-react-native"; +import { useSession } from "@/lib/auth/client"; +import { apiClient } from "@/lib/trpc/client"; +import { withPendingCloudMoves } from "../useCloudWorkspaceActions/pendingCloudMoves"; +import type { CloudWorkspaceRow } from "../useCloudWorkspaces"; +import { reviveCloudWorkspaceRows } from "../useCloudWorkspaces/reviveCloudWorkspaceRow"; + +const NO_ROWS: CloudWorkspaceRow[] = []; + +export function getArchivedCloudWorkspacesQueryKey( + organizationId: string | null, +) { + return [ + "cloud", + "cloudWorkspace", + "list", + organizationId, + "archived", + ] as const; +} + +export function useArchivedCloudWorkspaces({ + enabled = true, +}: { + enabled?: boolean; +} = {}): { + workspaces: CloudWorkspaceRow[]; + isReady: boolean; +} { + const enabledByFlag = Boolean(useFeatureFlag(FEATURE_FLAGS.CLOUD_WORKSPACES)); + const { data: session } = useSession(); + const organizationId = session?.session?.activeOrganizationId ?? null; + + const query = useQuery({ + queryKey: getArchivedCloudWorkspacesQueryKey(organizationId), + enabled: enabled && enabledByFlag && organizationId !== null, + networkMode: "always" as const, + retry: (count, error) => + !(error instanceof TRPCClientError && error.data?.code === "FORBIDDEN") && + count < 2, + select: reviveCloudWorkspaceRows, + queryFn: async (): Promise => { + if (!organizationId) return NO_ROWS; + try { + return withPendingCloudMoves( + "archived", + await apiClient.cloudWorkspace.list.query({ + organizationId, + archived: true, + }), + ); + } catch (error) { + if ( + error instanceof TRPCClientError && + error.data?.code === "FORBIDDEN" + ) { + return NO_ROWS; + } + throw error; + } + }, + }); + + return { + workspaces: query.data ?? NO_ROWS, + isReady: + !enabled || + !enabledByFlag || + organizationId === null || + query.isSuccess || + query.isError, + }; +} diff --git a/apps/mobile/hooks/useCloudWorkspaceActions/pendingCloudMoves.test.ts b/apps/mobile/hooks/useCloudWorkspaceActions/pendingCloudMoves.test.ts new file mode 100644 index 00000000000..9a203235b1b --- /dev/null +++ b/apps/mobile/hooks/useCloudWorkspaceActions/pendingCloudMoves.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, test } from "bun:test"; +import type { CloudWorkspaceRow } from "@/hooks/useCloudWorkspaces"; +import { + beginCloudMove, + endCloudMove, + withPendingCloudMoves, +} from "./pendingCloudMoves"; + +const row = (id: string, status: CloudWorkspaceRow["status"]) => + ({ id, status }) as CloudWorkspaceRow; + +describe("withPendingCloudMoves", () => { + test("keeps a row in the list it was just moved to until the move ends", () => { + const archiving = row("a", "deleted"); + beginCloudMove("archived", archiving); + + const active = withPendingCloudMoves("active", [ + row("a", "ready"), + row("b", "ready"), + ]); + const archived = withPendingCloudMoves("archived", []); + expect(active.map((r) => r.id)).toEqual(["b"]); + expect(archived).toEqual([archiving]); + + endCloudMove("a"); + expect( + withPendingCloudMoves("active", [row("a", "ready")]).map((r) => r.id), + ).toEqual(["a"]); + }); +}); diff --git a/apps/mobile/hooks/useCloudWorkspaceActions/pendingCloudMoves.ts b/apps/mobile/hooks/useCloudWorkspaceActions/pendingCloudMoves.ts new file mode 100644 index 00000000000..9094edfac20 --- /dev/null +++ b/apps/mobile/hooks/useCloudWorkspaceActions/pendingCloudMoves.ts @@ -0,0 +1,34 @@ +import type { CloudWorkspaceRow } from "@/hooks/useCloudWorkspaces"; + +type List = "active" | "archived"; + +const pending = new Map(); + +export function beginCloudMove(to: List, row: CloudWorkspaceRow): void { + pending.set(row.id, { to, row }); +} + +export function endCloudMove(id: string): void { + pending.delete(id); +} + +/** + * A list fetched while an archive or unarchive is in flight still has the row + * where it was; keep it where the user just moved it until the request settles. + */ +export function withPendingCloudMoves( + list: List, + rows: CloudWorkspaceRow[], +): CloudWorkspaceRow[] { + if (pending.size === 0) return rows; + const kept = rows.filter((row) => { + const move = pending.get(row.id); + return !move || move.to === list; + }); + for (const [id, move] of pending) { + if (move.to === list && !kept.some((row) => row.id === id)) { + kept.unshift(move.row); + } + } + return kept; +} diff --git a/apps/mobile/hooks/useCloudWorkspaceActions/useCloudWorkspaceActions.ts b/apps/mobile/hooks/useCloudWorkspaceActions/useCloudWorkspaceActions.ts index ec88fdf6f76..8829fdc5166 100644 --- a/apps/mobile/hooks/useCloudWorkspaceActions/useCloudWorkspaceActions.ts +++ b/apps/mobile/hooks/useCloudWorkspaceActions/useCloudWorkspaceActions.ts @@ -1,19 +1,26 @@ import { useQueryClient } from "@tanstack/react-query"; import { useCallback } from "react"; -import type { CloudWorkspaceRow } from "@/hooks/useCloudWorkspaces"; +import { getArchivedCloudWorkspacesQueryKey } from "@/hooks/useArchivedCloudWorkspaces"; +import { + type CloudWorkspaceRow, + getCloudWorkspacesQueryKey, +} from "@/hooks/useCloudWorkspaces"; +import { useSession } from "@/lib/auth/client"; import { clearSandboxAccess } from "@/lib/sandbox-access"; import { apiClient } from "@/lib/trpc/client"; +import { beginCloudMove, endCloudMove } from "./pendingCloudMoves"; const LIST_KEY = ["cloud", "cloudWorkspace", "list"]; /** - * Rename and delete for cloud workspaces go to the API, not the sandbox: the - * cloud row owns the name (the sandbox's copy is scratch), and deleting - * through the sandbox would remove the row inside it and leave the sandbox - * running — and billing — with the cloud row still listing it. + * Rename, archive and unarchive for cloud workspaces go to the API, not the + * sandbox: the cloud row owns the name (the sandbox's copy is scratch), and the + * API is what stops the sandbox and later deletes it. */ export function useCloudWorkspaceActions() { const queryClient = useQueryClient(); + const { data: session } = useSession(); + const organizationId = session?.session?.activeOrganizationId ?? null; const invalidate = useCallback( () => queryClient.invalidateQueries({ queryKey: LIST_KEY }), @@ -28,24 +35,71 @@ export function useCloudWorkspaceActions() { [invalidate], ); - const remove = useCallback( - async (id: string) => { - // The row goes when the user asks, not when the provider finishes - // tearing the sandbox down; the invalidate below is what restores it - // if the delete never landed. - queryClient.setQueriesData( - { queryKey: LIST_KEY }, - (rows) => rows?.filter((row) => row.id !== id), + /** Moves the row between the active and archived lists before the API answers. */ + const move = useCallback( + (id: string, to: "archived" | "active") => { + const active = getCloudWorkspacesQueryKey(organizationId); + const archived = getArchivedCloudWorkspacesQueryKey(organizationId); + const [from, into] = + to === "archived" ? [active, archived] : [archived, active]; + const row = queryClient + .getQueryData(from) + ?.find((candidate) => candidate.id === id); + queryClient.setQueryData(from, (rows) => + rows?.filter((candidate) => candidate.id !== id), ); + if (!row) return; + const moved: CloudWorkspaceRow = + to === "archived" + ? { + ...row, + status: "deleted", + deletedAt: new Date(), + sandboxUrl: null, + } + : { + ...row, + status: "provisioning", + deletedAt: null, + sandboxUrl: null, + }; + beginCloudMove(to, moved); + queryClient.setQueryData(into, (rows) => [ + moved, + ...(rows ?? []).filter((candidate) => candidate.id !== id), + ]); + }, + [organizationId, queryClient], + ); + + const archive = useCallback( + async (id: string) => { + await queryClient.cancelQueries({ queryKey: LIST_KEY }); + move(id, "archived"); try { await apiClient.cloudWorkspace.delete.mutate({ id }); clearSandboxAccess(id); } finally { + endCloudMove(id); + await invalidate(); + } + }, + [invalidate, move, queryClient], + ); + + const unarchive = useCallback( + async (id: string) => { + await queryClient.cancelQueries({ queryKey: LIST_KEY }); + move(id, "active"); + try { + await apiClient.cloudWorkspace.unarchive.mutate({ id }); + } finally { + endCloudMove(id); await invalidate(); } }, - [invalidate, queryClient], + [invalidate, move, queryClient], ); - return { rename, remove }; + return { rename, archive, unarchive }; } diff --git a/apps/mobile/hooks/useCloudWorkspaceItems/index.ts b/apps/mobile/hooks/useCloudWorkspaceItems/index.ts index e34ffcc58de..cad277d3b03 100644 --- a/apps/mobile/hooks/useCloudWorkspaceItems/index.ts +++ b/apps/mobile/hooks/useCloudWorkspaceItems/index.ts @@ -2,5 +2,6 @@ export { type CloudWorkspaceItem, type CloudWorkspaceItemsValue, type CloudWorkspaceStatus, + itemFromCloudRow, useCloudWorkspaceItems, } from "./useCloudWorkspaceItems"; diff --git a/apps/mobile/hooks/useCloudWorkspaceItems/useCloudWorkspaceItems.ts b/apps/mobile/hooks/useCloudWorkspaceItems/useCloudWorkspaceItems.ts index 9d05339eadf..a18a7c5c677 100644 --- a/apps/mobile/hooks/useCloudWorkspaceItems/useCloudWorkspaceItems.ts +++ b/apps/mobile/hooks/useCloudWorkspaceItems/useCloudWorkspaceItems.ts @@ -29,7 +29,7 @@ export interface CloudWorkspaceItem extends HostWorkspaceItem { * sandbox self-seeds its workspace as `local`. `hostReachable` is false so * decoration that needs the host (diff stats) waits for the workspace to open. */ -function itemFromCloudRow(cloud: CloudWorkspaceRow): CloudWorkspaceItem { +export function itemFromCloudRow(cloud: CloudWorkspaceRow): CloudWorkspaceItem { return { id: cloud.id, organizationId: cloud.organizationId, diff --git a/apps/mobile/hooks/useCloudWorkspaces/reviveCloudWorkspaceRow.ts b/apps/mobile/hooks/useCloudWorkspaces/reviveCloudWorkspaceRow.ts new file mode 100644 index 00000000000..771a08cadbd --- /dev/null +++ b/apps/mobile/hooks/useCloudWorkspaces/reviveCloudWorkspaceRow.ts @@ -0,0 +1,29 @@ +import type { CloudWorkspaceRow } from "./useCloudWorkspaces"; + +const asDate = (value: Value): Value => + (value === null || value instanceof Date + ? value + : new Date(value as unknown as string)) as Value; + +/** + * The persisted query cache stores rows as JSON, so a cold start hands back + * strings where the API sent dates; every date field is a Date again after this. + */ +export function reviveCloudWorkspaceRow( + row: CloudWorkspaceRow, +): CloudWorkspaceRow { + return { + ...row, + createdAt: asDate(row.createdAt), + updatedAt: asDate(row.updatedAt), + deletedAt: asDate(row.deletedAt), + agentStatusAt: asDate(row.agentStatusAt), + presence: row.presence.map((person) => ({ + ...person, + lastSeenAt: asDate(person.lastSeenAt), + })), + }; +} + +export const reviveCloudWorkspaceRows = (rows: CloudWorkspaceRow[]) => + rows.map(reviveCloudWorkspaceRow); diff --git a/apps/mobile/hooks/useCloudWorkspaces/useCloudWorkspaces.ts b/apps/mobile/hooks/useCloudWorkspaces/useCloudWorkspaces.ts index 785c901fb97..eae2cf91555 100644 --- a/apps/mobile/hooks/useCloudWorkspaces/useCloudWorkspaces.ts +++ b/apps/mobile/hooks/useCloudWorkspaces/useCloudWorkspaces.ts @@ -7,6 +7,8 @@ import { useEffect } from "react"; import { useSession } from "@/lib/auth/client"; import { pruneSandboxAccess } from "@/lib/sandbox-access"; import { apiClient } from "@/lib/trpc/client"; +import { withPendingCloudMoves } from "../useCloudWorkspaceActions/pendingCloudMoves"; +import { reviveCloudWorkspaceRows } from "./reviveCloudWorkspaceRow"; export type CloudWorkspaceRow = RouterOutputs["cloudWorkspace"]["list"][number]; @@ -57,10 +59,14 @@ export function useCloudWorkspaces(): CloudWorkspacesValue { current.state.data?.some((row) => row.status === "provisioning") ? PROVISIONING_POLL_MS : IDLE_POLL_MS, + select: reviveCloudWorkspaceRows, queryFn: async (): Promise => { if (!organizationId) return NO_ROWS; try { - return await apiClient.cloudWorkspace.list.query({ organizationId }); + return withPendingCloudMoves( + "active", + await apiClient.cloudWorkspace.list.query({ organizationId }), + ); } catch (error) { if ( error instanceof TRPCClientError && diff --git a/apps/mobile/hooks/useDeleteWorkspace/useDeleteWorkspace.ts b/apps/mobile/hooks/useDeleteWorkspace/useDeleteWorkspace.ts index b114f92a21f..0b453d4c5cb 100644 --- a/apps/mobile/hooks/useDeleteWorkspace/useDeleteWorkspace.ts +++ b/apps/mobile/hooks/useDeleteWorkspace/useDeleteWorkspace.ts @@ -2,7 +2,6 @@ import { useLingui } from "@lingui/react/macro"; import { useQueryClient } from "@tanstack/react-query"; import { useCallback } from "react"; import { Alert } from "react-native"; -import { useCloudWorkspaceActions } from "@/hooks/useCloudWorkspaceActions"; import { getHostWorkspacesQueryKey, type HostWorkspaceRow, @@ -15,12 +14,10 @@ export interface DeleteWorkspaceTarget { id: string; name: string; type: HostWorkspaceRow["type"]; - /** Host owning the worktree. Unused for a cloud workspace. */ + /** Host owning the worktree. */ hostId: string | null; /** Where that host answers — null when it is offline. */ hostUrl: string | null; - /** Only the API can destroy a cloud workspace; its sandbox cannot. */ - isCloud: boolean; } /** Codes the relay answers with itself, about its tunnel and not the delete. */ @@ -49,44 +46,9 @@ const RELAY_ERROR_CODES = new Set(["BAD_GATEWAY", "SERVICE_UNAVAILABLE"]); export function useDeleteWorkspace() { const { t } = useLingui(); const queryClient = useQueryClient(); - const cloud = useCloudWorkspaceActions(); return useCallback( (target: DeleteWorkspaceTarget, onConfirmed?: () => void) => { - if (target.isCloud) { - Alert.alert( - t({ - message: "Delete cloud workspace", - }), - t({ - message: `Delete "${target.name}"? This shuts down its sandbox and everything in it.`, - }), - [ - { - style: "cancel", - text: t({ message: "Cancel" }), - }, - { - onPress: () => { - onConfirmed?.(); - void cloud.remove(target.id).catch(() => - Alert.alert( - t({ - message: "Delete failed", - }), - ), - ); - }, - style: "destructive", - text: t({ - message: "Delete", - }), - }, - ], - ); - return; - } - const { hostId, hostUrl } = target; if (!hostId || !hostUrl) { Alert.alert( @@ -176,7 +138,7 @@ export function useDeleteWorkspace() { ], ); }, - [cloud, queryClient, t], + [queryClient, t], ); } diff --git a/apps/mobile/screens/(authenticated)/(home)/filter/creator/CreatorFilterScreen.tsx b/apps/mobile/screens/(authenticated)/(home)/filter/creator/CreatorFilterScreen.tsx new file mode 100644 index 00000000000..491597c3eb3 --- /dev/null +++ b/apps/mobile/screens/(authenticated)/(home)/filter/creator/CreatorFilterScreen.tsx @@ -0,0 +1,124 @@ +import { useLingui } from "@lingui/react/macro"; +import { useRouter } from "expo-router"; +import { Users } from "lucide-react-native"; +import { useMemo } from "react"; +import { ScrollView } from "react-native"; +import { Icon } from "@/components/ui/icon"; +import { useArchivedCloudWorkspaces } from "@/hooks/useArchivedCloudWorkspaces"; +import { useCloudWorkspaces } from "@/hooks/useCloudWorkspaces"; +import { useSession } from "@/lib/auth/client"; +import { posthog } from "@/lib/posthog"; +import { + type CloudCreatorFilter, + useWorkspacesFilterStore, +} from "@/screens/(authenticated)/(home)/home/stores/workspacesFilterStore"; +import { ListRow } from "@/screens/(authenticated)/components/ListRow"; +import { ListRowCheck } from "@/screens/(authenticated)/components/ListRowCheck"; +import { UserAvatar } from "@/screens/(authenticated)/settings/components/UserAvatar"; + +export function CreatorFilterScreen() { + const { t } = useLingui(); + const router = useRouter(); + const { data: session } = useSession(); + const myId = session?.user?.id ?? null; + const creator = useWorkspacesFilterStore((store) => store.cloudCreator); + const setCloudCreator = useWorkspacesFilterStore( + (store) => store.setCloudCreator, + ); + const { workspaces: active } = useCloudWorkspaces(); + const { workspaces: archived } = useArchivedCloudWorkspaces(); + + const people = useMemo(() => { + const byId = new Map< + string, + { userId: string; name: string; image: string | null; count: number } + >(); + for (const row of [...active, ...archived]) { + const person = row.createdBy; + if (!person?.userId || person.userId === myId) continue; + const existing = byId.get(person.userId); + if (existing) existing.count += 1; + else + byId.set(person.userId, { + userId: person.userId, + name: person.name ?? "", + image: person.image ?? null, + count: 1, + }); + } + return [...byId.values()].sort((a, b) => b.count - a.count); + }, [active, archived, myId]); + + const pick = (value: CloudCreatorFilter) => { + setCloudCreator(value); + posthog.capture("filter_applied", { + filter: "cloud_creator", + value: typeof value === "string" ? value : "person", + }); + router.back(); + }; + + return ( + + + } + label={t({ message: "Created by me" })} + trailing={} + onPress={() => pick("me")} + /> + + } + label={t({ message: "Anyone" })} + trailing={} + onPress={() => pick("anyone")} + isLast={people.length === 0} + /> + {people.map((person, index) => ( + + } + label={person.name} + trailing={ + + } + onPress={() => + pick({ + userId: person.userId, + name: person.name, + image: person.image, + }) + } + isLast={index === people.length - 1} + /> + ))} + + ); +} diff --git a/apps/mobile/screens/(authenticated)/(home)/filter/creator/index.ts b/apps/mobile/screens/(authenticated)/(home)/filter/creator/index.ts new file mode 100644 index 00000000000..a1bc1a9fd80 --- /dev/null +++ b/apps/mobile/screens/(authenticated)/(home)/filter/creator/index.ts @@ -0,0 +1 @@ +export { CreatorFilterScreen } from "./CreatorFilterScreen"; diff --git a/apps/mobile/screens/(authenticated)/(home)/filter/status/StatusFilterScreen.tsx b/apps/mobile/screens/(authenticated)/(home)/filter/status/StatusFilterScreen.tsx new file mode 100644 index 00000000000..6499e68346a --- /dev/null +++ b/apps/mobile/screens/(authenticated)/(home)/filter/status/StatusFilterScreen.tsx @@ -0,0 +1,88 @@ +import { useLingui } from "@lingui/react/macro"; +import { useFormat } from "@superset/i18n/react"; +import { useRouter } from "expo-router"; +import { Archive } from "lucide-react-native"; +import { View } from "react-native"; +import { Icon } from "@/components/ui/icon"; +import { Text } from "@/components/ui/text"; +import { useArchivedCloudWorkspaces } from "@/hooks/useArchivedCloudWorkspaces"; +import { useCloudWorkspaces } from "@/hooks/useCloudWorkspaces"; +import { useTheme } from "@/hooks/useTheme"; +import { posthog } from "@/lib/posthog"; +import { + type CloudStatusFilter, + useWorkspacesFilterStore, +} from "@/screens/(authenticated)/(home)/home/stores/workspacesFilterStore"; +import { useCloudFilters } from "@/screens/(authenticated)/(home)/hooks/useCloudFilters"; +import { CloudIcon } from "@/screens/(authenticated)/components/CloudIcon"; +import { ListRow } from "@/screens/(authenticated)/components/ListRow"; +import { ListRowCheck } from "@/screens/(authenticated)/components/ListRowCheck"; + +export function StatusFilterScreen() { + const { t } = useLingui(); + const router = useRouter(); + const theme = useTheme(); + const { formatNumber } = useFormat(); + const setCloudStatus = useWorkspacesFilterStore( + (store) => store.setCloudStatus, + ); + const { status, matchesCreator } = useCloudFilters(); + const { workspaces: active } = useCloudWorkspaces(); + const { workspaces: archived } = useArchivedCloudWorkspaces(); + + const options: { + value: CloudStatusFilter; + label: string; + icon: typeof Archive; + count: number; + }[] = [ + { + value: "active", + label: t({ message: "Active", context: "cloud workspace status" }), + icon: CloudIcon, + count: active.filter((row) => matchesCreator(row.createdByUserId)).length, + }, + { + value: "archived", + label: t({ message: "Archived" }), + icon: Archive, + count: archived.filter((row) => matchesCreator(row.createdByUserId)) + .length, + }, + ]; + + return ( + + {options.map((option, index) => ( + + } + label={option.label} + trailing={ + <> + + {formatNumber(option.count)} + + + + } + onPress={() => { + setCloudStatus(option.value); + posthog.capture("filter_applied", { + filter: "cloud_status", + value: option.value, + }); + router.back(); + }} + isLast={index === options.length - 1} + /> + ))} + + ); +} diff --git a/apps/mobile/screens/(authenticated)/(home)/filter/status/index.ts b/apps/mobile/screens/(authenticated)/(home)/filter/status/index.ts new file mode 100644 index 00000000000..9d2c7ae9208 --- /dev/null +++ b/apps/mobile/screens/(authenticated)/(home)/filter/status/index.ts @@ -0,0 +1 @@ +export { StatusFilterScreen } from "./StatusFilterScreen"; diff --git a/apps/mobile/screens/(authenticated)/(home)/home/HomeScreen.tsx b/apps/mobile/screens/(authenticated)/(home)/home/HomeScreen.tsx index 69afdf95f11..c35405db48c 100644 --- a/apps/mobile/screens/(authenticated)/(home)/home/HomeScreen.tsx +++ b/apps/mobile/screens/(authenticated)/(home)/home/HomeScreen.tsx @@ -1,6 +1,8 @@ import { LegendList } from "@legendapp/list/react-native"; import { useLingui } from "@lingui/react/macro"; import { i18n } from "@superset/i18n"; +import { useFormat } from "@superset/i18n/react"; +import { groupCloudWorkspacesByTime } from "@superset/shared/cloud-workspace-groups"; import { FEATURE_FLAGS } from "@superset/shared/constants"; import { getWorkspaceActivityTime } from "@superset/shared/workspace-activity"; import { useQueryClient } from "@tanstack/react-query"; @@ -21,16 +23,20 @@ import { import { useSafeAreaInsets } from "react-native-safe-area-context"; import { Icon } from "@/components/ui/icon"; import { Text } from "@/components/ui/text"; +import { useArchivedCloudWorkspaces } from "@/hooks/useArchivedCloudWorkspaces"; +import { useCloudWorkspaceItems } from "@/hooks/useCloudWorkspaceItems"; import { - type CloudWorkspaceStatus, - useCloudWorkspaceItems, -} from "@/hooks/useCloudWorkspaceItems"; + type CloudWorkspaceRow, + useCloudWorkspaces, +} from "@/hooks/useCloudWorkspaces"; import { useHostProjects } from "@/hooks/useHostProjects"; import { type HostWorkspaceItem, useHostWorkspaces, } from "@/hooks/useHostWorkspaces"; import { useOrgHosts } from "@/hooks/useOrgHosts"; +import { useSession } from "@/lib/auth/client"; +import { useCloudFilters } from "@/screens/(authenticated)/(home)/hooks/useCloudFilters"; import { useSelectedHost } from "@/screens/(authenticated)/(home)/hooks/useSelectedHost"; import { useWorkspaceScope } from "@/screens/(authenticated)/(home)/hooks/useWorkspaceScope"; import { HeaderNotice } from "@/screens/(authenticated)/components/HeaderNotice"; @@ -41,22 +47,29 @@ import { } from "@/screens/(authenticated)/hooks/usePullRequests"; import { usePinnedWorkspacesStore } from "@/screens/(authenticated)/stores/pinnedWorkspacesStore"; import { pullRequestStatus } from "@/screens/(authenticated)/workspace/[id]/utils/pullRequest"; +import { CloudWorkspaceRow as CloudWorkspaceListRow } from "./components/CloudWorkspaceRow"; import { HostOfflineView } from "./components/HostOfflineView"; import { NewChatWidget } from "./components/NewChatWidget"; import { targetKeyFor } from "./components/NewChatWidget/hooks/useNewChatTargets"; import { useNewSessionPreferencesStore } from "./components/NewChatWidget/stores/newSessionPreferencesStore"; import { OrganizationHeaderButton } from "./components/OrganizationHeaderButton"; +import { PeriodHeader } from "./components/PeriodHeader"; import { ProjectSectionHeader } from "./components/ProjectSectionHeader"; import { ScopeBar } from "./components/ScopeBar"; import { WorkspaceRow } from "./components/WorkspaceRow"; import { useAgentLiveActivity } from "./hooks/useAgentLiveActivity"; import { useAppReviewPrompt } from "./hooks/useAppReviewPrompt"; -import { useCloudRepoPrefixes } from "./hooks/useCloudRepoPrefixes"; +import { + cloudPullRequestRefKey, + useCloudPullRequests, +} from "./hooks/useCloudPullRequests"; +import { useCloudRepos } from "./hooks/useCloudRepos"; import { useFirstPaint } from "./hooks/useFirstPaint"; import { type TerminalsHost, useHostsTerminals, } from "./hooks/useHostTerminals"; +import { useNow } from "./hooks/useNow"; import { useVisibleDiffStats } from "./hooks/useVisibleDiffStats"; import { collapsedProjectKey, @@ -100,13 +113,18 @@ type HomeListItem = | { kind: "workspace"; workspace: HostWorkspaceItem; - cloudStatus?: CloudWorkspaceStatus; } | { kind: "hostOffline"; hostName: string; lastSeenAt: number | null | undefined; - }; + } + | { + kind: "periodHeader"; + key: string; + label: string; + } + | { kind: "cloudRow"; row: CloudWorkspaceRow }; function homeListItemKey(item: HomeListItem): string { switch (item.kind) { @@ -114,6 +132,10 @@ function homeListItemKey(item: HomeListItem): string { return `project:${item.projectId}`; case "workspace": return `ws:${item.workspace.id}`; + case "periodHeader": + return `period:${item.key}`; + case "cloudRow": + return `cloud:${item.row.id}`; default: return "host-offline"; } @@ -169,6 +191,14 @@ export function HomeScreen() { isReady: cloudReady, } = useCloudWorkspaceItems(); const cloudScope = useWorkspaceScope() === "cloud"; + const cloudFilters = useCloudFilters(); + const showArchived = cloudScope && cloudFilters.status === "archived"; + const { workspaces: archivedCloud, isReady: archivedReady } = + useArchivedCloudWorkspaces({ enabled: showArchived }); + const { workspaces: activeCloud } = useCloudWorkspaces(); + const { formatRelativePeriod } = useFormat(); + const viewerId = useSession().data?.user?.id ?? null; + const now = useNow(30_000); // No session marks for cloud rows: a request per sandbox keeps each one // awake for as long as Home is on screen. const terminalHosts = useMemo( @@ -209,7 +239,9 @@ export function HomeScreen() { !isLoadingOrganizations && (!activeOrganizationId || !hostsQuery.isPending) && (cloudScope - ? cloudReady + ? showArchived + ? archivedReady + : cloudReady : !presencePending && workspacesReady && projectsReady); const hasPainted = useFirstPaint(contentReady); @@ -262,15 +294,26 @@ export function HomeScreen() { const listItems = useMemo(() => { const items: HomeListItem[] = []; - // Under Cloud the sandboxes are the whole list: flat, no project headers - // to group them by and no machine to be offline. + // Under Cloud the list is desktop's: grouped by when each workspace last + // heard from its agent (or was archived), newest first. if (cloudScope) { - for (const workspace of [...cloudItems].sort(byPinThenActivity)) { + const rows = (showArchived ? archivedCloud : activeCloud).filter((row) => + cloudFilters.matchesCreator(row.createdByUserId), + ); + const groups = groupCloudWorkspacesByTime({ + workspaces: rows, + now, + sort: sort === "createdAt" ? "created" : "activity", + at: showArchived ? (row) => row.deletedAt ?? row.createdAt : undefined, + }); + for (const { period, workspaces: grouped } of groups) { + const key = `${showArchived ? "archived" : "active"}:${period.unit}:${period.count}`; items.push({ - kind: "workspace", - workspace, - cloudStatus: workspace.cloud.status, + kind: "periodHeader", + key, + label: formatRelativePeriod(period), }); + for (const row of grouped) items.push({ kind: "cloudRow", row }); } return items; } @@ -364,8 +407,14 @@ export function HomeScreen() { return items; }, [ liveWorkspaces, - cloudItems, cloudScope, + showArchived, + archivedCloud, + activeCloud, + cloudFilters.matchesCreator, + sort, + formatRelativePeriod, + now, selectedHost, projects, byPinThenActivity, @@ -486,7 +535,20 @@ export function HomeScreen() { // Projects are fully local: PR rows are matched by repo coordinates // parsed from the PR URL (cloud repo UUIDs aren't known host-side). // Cloud rows' projects come from the API instead. - const cloudRepoPrefixes = useCloudRepoPrefixes(); + const cloudRepos = useCloudRepos(); + const cloudPullRequests = useCloudPullRequests( + useMemo( + () => + listItems.flatMap((item) => { + if (item.kind !== "cloudRow") return []; + const repoFullName = cloudRepos.get(item.row.id); + return repoFullName + ? [{ repoFullName, headBranch: item.row.branch }] + : []; + }), + [listItems, cloudRepos], + ), + ); const repoPrefixesByProject = useMemo( () => new Map([ @@ -502,6 +564,32 @@ export function HomeScreen() { const renderItem = useCallback( ({ item }: { item: HomeListItem }) => { + if (item.kind === "periodHeader") { + return ; + } + if (item.kind === "cloudRow") { + const repoFullName = cloudRepos.get(item.row.id); + return ( + + ); + } if (item.kind === "hostOffline") { return ( @@ -548,12 +636,10 @@ export function HomeScreen() { /> ); } - const { workspace, cloudStatus } = item; - const repoPrefix = cloudStatus - ? (cloudRepoPrefixes.get(workspace.id) ?? null) - : workspace.projectId - ? repoPrefixesByProject.get(workspace.projectId) - : undefined; + const { workspace } = item; + const repoPrefix = workspace.projectId + ? repoPrefixesByProject.get(workspace.projectId) + : undefined; return ( ); }, [ pullRequestsByRepoBranch, - cloudRepoPrefixes, + cloudRepos, + cloudPullRequests, + viewerId, + now, repoPrefixesByProject, diffStats, cache, @@ -601,7 +689,7 @@ export function HomeScreen() { scope={cloudScope ? "cloud" : "host"} hostName={selectedHost?.name ?? null} hostOnline={selectedHost?.isOnline ?? false} - sortLabel={sortLabel} + sortLabel={showArchived ? null : sortLabel} onPressScope={() => { void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); router.push("/(authenticated)/(home)/filter/scope"); @@ -708,13 +796,19 @@ export function HomeScreen() { contentReady ? ( - {cloudScope - ? t({ - message: "No cloud workspaces yet", - }) - : t({ - message: "No projects on this host yet", - })} + {showArchived + ? t({ message: "No archived workspaces" }) + : cloudScope && activeCloud.length > 0 + ? t({ + message: "No cloud workspaces match these filters", + }) + : cloudScope + ? t({ + message: "No cloud workspaces yet", + }) + : t({ + message: "No projects on this host yet", + })} ) : ( diff --git a/apps/mobile/screens/(authenticated)/(home)/home/components/CloudWorkspaceRow/CloudWorkspaceRow.tsx b/apps/mobile/screens/(authenticated)/(home)/home/components/CloudWorkspaceRow/CloudWorkspaceRow.tsx new file mode 100644 index 00000000000..f1a8dd0d61e --- /dev/null +++ b/apps/mobile/screens/(authenticated)/(home)/home/components/CloudWorkspaceRow/CloudWorkspaceRow.tsx @@ -0,0 +1,152 @@ +import { useLingui } from "@lingui/react/macro"; +import { useRouter } from "expo-router"; +import { Pressable, View } from "react-native"; +import { Button } from "@/components/ui/button"; +import { Icon } from "@/components/ui/icon"; +import { Text } from "@/components/ui/text"; +import type { CloudWorkspaceRow as CloudWorkspaceRowData } from "@/hooks/useCloudWorkspaces"; +import type { HostWorkspacesCacheOps } from "@/hooks/useHostWorkspaces"; +import { cn } from "@/lib/utils"; +import { ProjectAvatar } from "@/screens/(authenticated)/(home)/filter/components/ProjectAvatar"; +import { UserAvatar } from "@/screens/(authenticated)/settings/components/UserAvatar"; +import { useUnreadWorkspacesStore } from "@/screens/(authenticated)/stores/unreadWorkspacesStore"; +import { + PULL_REQUEST_STATUS, + pullRequestStatus, +} from "@/screens/(authenticated)/workspace/[id]/utils/pullRequest"; +import type { CloudPullRequest } from "../../hooks/useCloudPullRequests"; +import { ArchivedCloudRowMenu } from "./components/ArchivedCloudRowMenu"; +import { CloudWorkspaceRowMenu } from "./components/CloudWorkspaceRowMenu"; +import { CloudWorkspaceStatus } from "./components/CloudWorkspaceStatus"; + +/** Desktop's DashboardSidebarCloudRow: repo, name, owner if not you, PR, status. */ +export function CloudWorkspaceRow({ + row, + repoFullName, + pullRequest, + viewerId, + now, + cache, + onCopied, +}: { + row: CloudWorkspaceRowData; + repoFullName?: string; + pullRequest?: CloudPullRequest; + viewerId: string | null; + now: Date; + cache: HostWorkspacesCacheOps; + onCopied: () => void; +}) { + const { t } = useLingui(); + const router = useRouter(); + const archived = row.status === "deleted"; + const readAt = useUnreadWorkspacesStore((state) => state.cloudReadAt[row.id]); + const manuallyUnread = useUnreadWorkspacesStore( + (state) => row.id in state.manualUnread, + ); + const setManualUnread = useUnreadWorkspacesStore( + (state) => state.setManualUnread, + ); + const clearManualUnread = useUnreadWorkspacesStore( + (state) => state.clearManualUnread, + ); + const markCloudRead = useUnreadWorkspacesStore( + (state) => state.markCloudRead, + ); + const isRead = + !manuallyUnread && + (row.agentStatusAt === null || + (readAt ?? 0) >= row.agentStatusAt.getTime()); + const isUnread = manuallyUnread || (row.agentStatus === "review" && !isRead); + const toggleUnread = () => { + if (!isUnread) { + setManualUnread(row.id); + return; + } + clearManualUnread(row.id); + if (row.agentStatusAt) markCloudRead(row.id, row.agentStatusAt.getTime()); + }; + const owner = row.createdBy; + const isMine = owner !== null && owner.userId === viewerId; + const [repoOwner, repoName] = repoFullName?.split("/") ?? []; + const prStatus = pullRequest + ? PULL_REQUEST_STATUS[pullRequestStatus({ ...pullRequest, mergedAt: null })] + : null; + + const body = ( + router.push(`/(authenticated)/workspace/${row.id}`)} + ph-label="cloud-workspace-row" + > + + + + {row.name || t({ message: "Untitled workspace" })} + + {owner && !isMine ? ( + + ) : null} + + {pullRequest && prStatus && !archived ? ( + + ) : null} + + + + + ); + + return archived ? ( + + {body} + + ) : ( + + {body} + + ); +} diff --git a/apps/mobile/screens/(authenticated)/(home)/home/components/CloudWorkspaceRow/components/ArchivedCloudRowMenu/ArchivedCloudRowMenu.tsx b/apps/mobile/screens/(authenticated)/(home)/home/components/CloudWorkspaceRow/components/ArchivedCloudRowMenu/ArchivedCloudRowMenu.tsx new file mode 100644 index 00000000000..96e189d8f44 --- /dev/null +++ b/apps/mobile/screens/(authenticated)/(home)/home/components/CloudWorkspaceRow/components/ArchivedCloudRowMenu/ArchivedCloudRowMenu.tsx @@ -0,0 +1,60 @@ +import { useLingui } from "@lingui/react/macro"; +import { Link } from "expo-router"; +import type { ReactNode } from "react"; +import { Alert } from "react-native"; +import { useCloudWorkspaceActions } from "@/hooks/useCloudWorkspaceActions"; +import { itemFromCloudRow } from "@/hooks/useCloudWorkspaceItems"; +import type { CloudWorkspaceRow } from "@/hooks/useCloudWorkspaces"; +import type { HostWorkspacesCacheOps } from "@/hooks/useHostWorkspaces"; +import { useWorkspaceRowActions } from "../../../WorkspaceRow/hooks/useWorkspaceRowActions"; + +export function ArchivedCloudRowMenu({ + row, + cache, + onCopied, + children, +}: { + row: CloudWorkspaceRow; + cache: HostWorkspacesCacheOps; + onCopied: () => void; + children: ReactNode; +}) { + const { t } = useLingui(); + const { unarchive } = useCloudWorkspaceActions(); + const { copyId, shareWorkspace } = useWorkspaceRowActions( + itemFromCloudRow(row), + cache, + [], + row.status, + onCopied, + ); + return ( + event.preventDefault()} + asChild + > + {children} + + + void unarchive(row.id).catch(() => + Alert.alert(t({ message: "Unarchive failed" })), + ) + } + > + {t({ message: "Unarchive" })} + + + + {t({ message: "Copy ID" })} + + + {t({ message: "Share" })} + + + + + ); +} diff --git a/apps/mobile/screens/(authenticated)/(home)/home/components/CloudWorkspaceRow/components/ArchivedCloudRowMenu/index.ts b/apps/mobile/screens/(authenticated)/(home)/home/components/CloudWorkspaceRow/components/ArchivedCloudRowMenu/index.ts new file mode 100644 index 00000000000..6b0c489d360 --- /dev/null +++ b/apps/mobile/screens/(authenticated)/(home)/home/components/CloudWorkspaceRow/components/ArchivedCloudRowMenu/index.ts @@ -0,0 +1 @@ +export { ArchivedCloudRowMenu } from "./ArchivedCloudRowMenu"; diff --git a/apps/mobile/screens/(authenticated)/(home)/home/components/CloudWorkspaceRow/components/CloudWorkspaceRowMenu/CloudWorkspaceRowMenu.tsx b/apps/mobile/screens/(authenticated)/(home)/home/components/CloudWorkspaceRow/components/CloudWorkspaceRowMenu/CloudWorkspaceRowMenu.tsx new file mode 100644 index 00000000000..ed6959aba52 --- /dev/null +++ b/apps/mobile/screens/(authenticated)/(home)/home/components/CloudWorkspaceRow/components/CloudWorkspaceRowMenu/CloudWorkspaceRowMenu.tsx @@ -0,0 +1,47 @@ +import type { ReactNode } from "react"; +import { itemFromCloudRow } from "@/hooks/useCloudWorkspaceItems"; +import type { CloudWorkspaceRow } from "@/hooks/useCloudWorkspaces"; +import type { HostWorkspacesCacheOps } from "@/hooks/useHostWorkspaces"; +import { WorkspaceRowMenu } from "../../../WorkspaceRow/components/WorkspaceRowMenu"; +import { useWorkspaceRowActions } from "../../../WorkspaceRow/hooks/useWorkspaceRowActions"; + +export function CloudWorkspaceRowMenu({ + row, + cache, + isUnread, + onToggleUnread, + onCopied, + children, +}: { + row: CloudWorkspaceRow; + cache: HostWorkspacesCacheOps; + isUnread: boolean; + onToggleUnread: () => void; + onCopied: () => void; + children: ReactNode; +}) { + const { renameWorkspace, deleteWorkspace, copyId, shareWorkspace } = + useWorkspaceRowActions( + itemFromCloudRow(row), + cache, + [], + row.status, + onCopied, + ); + return ( + void renameWorkspace()} + onDelete={deleteWorkspace} + onCopyId={copyId} + onShare={shareWorkspace} + > + {children} + + ); +} diff --git a/apps/mobile/screens/(authenticated)/(home)/home/components/CloudWorkspaceRow/components/CloudWorkspaceRowMenu/index.ts b/apps/mobile/screens/(authenticated)/(home)/home/components/CloudWorkspaceRow/components/CloudWorkspaceRowMenu/index.ts new file mode 100644 index 00000000000..1513b6052d7 --- /dev/null +++ b/apps/mobile/screens/(authenticated)/(home)/home/components/CloudWorkspaceRow/components/CloudWorkspaceRowMenu/index.ts @@ -0,0 +1 @@ +export { CloudWorkspaceRowMenu } from "./CloudWorkspaceRowMenu"; diff --git a/apps/mobile/screens/(authenticated)/(home)/home/components/CloudWorkspaceRow/components/CloudWorkspaceStatus/CloudWorkspaceStatus.tsx b/apps/mobile/screens/(authenticated)/(home)/home/components/CloudWorkspaceRow/components/CloudWorkspaceStatus/CloudWorkspaceStatus.tsx new file mode 100644 index 00000000000..770dccce593 --- /dev/null +++ b/apps/mobile/screens/(authenticated)/(home)/home/components/CloudWorkspaceRow/components/CloudWorkspaceStatus/CloudWorkspaceStatus.tsx @@ -0,0 +1,95 @@ +import { useLingui } from "@lingui/react/macro"; +import * as Haptics from "expo-haptics"; +import { Info, RotateCcw } from "lucide-react-native"; +import { Alert, Pressable, View } from "react-native"; +import { Icon } from "@/components/ui/icon"; +import { Text } from "@/components/ui/text"; +import { useCloudWorkspaceActions } from "@/hooks/useCloudWorkspaceActions"; +import type { CloudWorkspaceRow } from "@/hooks/useCloudWorkspaces"; +import { useTheme } from "@/hooks/useTheme"; +import { PingDot } from "@/screens/(authenticated)/components/PingDot"; +import { WaveSpinner } from "@/screens/(authenticated)/components/WaveSpinner"; +import { compactTime } from "@/screens/(authenticated)/workspace/[id]/utils/compactTime"; + +/** Desktop's CloudWorkspaceStatus, with Unarchive in reach instead of on hover. */ +export function CloudWorkspaceStatus({ + row, + isUnread, + now, +}: { + row: CloudWorkspaceRow; + isUnread: boolean; + now: Date; +}) { + const { t } = useLingui(); + const theme = useTheme(); + const { unarchive } = useCloudWorkspaceActions(); + + if (row.status === "provisioning") { + return ( + + + + ); + } + if (row.status === "deleted") { + return ( + { + void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + void unarchive(row.id).catch(() => + Alert.alert(t({ message: "Unarchive failed" })), + ); + }} + ph-label="cloud-row-unarchive" + > + + + ); + } + if (row.status === "failed" || row.agentStatus === "failed") { + return ( + + ); + } + if (row.agentStatus === "permission") { + return ( + + + + ); + } + if (row.agentStatus === "working") { + return ( + + + + ); + } + if (isUnread) { + return ( + + ); + } + if (!row.agentStatusAt) return null; + return ( + + {compactTime(row.agentStatusAt.getTime(), now.getTime())} + + ); +} diff --git a/apps/mobile/screens/(authenticated)/(home)/home/components/CloudWorkspaceRow/components/CloudWorkspaceStatus/index.ts b/apps/mobile/screens/(authenticated)/(home)/home/components/CloudWorkspaceRow/components/CloudWorkspaceStatus/index.ts new file mode 100644 index 00000000000..4af6742a120 --- /dev/null +++ b/apps/mobile/screens/(authenticated)/(home)/home/components/CloudWorkspaceRow/components/CloudWorkspaceStatus/index.ts @@ -0,0 +1 @@ +export { CloudWorkspaceStatus } from "./CloudWorkspaceStatus"; diff --git a/apps/mobile/screens/(authenticated)/(home)/home/components/CloudWorkspaceRow/index.ts b/apps/mobile/screens/(authenticated)/(home)/home/components/CloudWorkspaceRow/index.ts new file mode 100644 index 00000000000..9c7f4b1f68c --- /dev/null +++ b/apps/mobile/screens/(authenticated)/(home)/home/components/CloudWorkspaceRow/index.ts @@ -0,0 +1 @@ +export { CloudWorkspaceRow } from "./CloudWorkspaceRow"; diff --git a/apps/mobile/screens/(authenticated)/(home)/home/components/PeriodHeader/PeriodHeader.tsx b/apps/mobile/screens/(authenticated)/(home)/home/components/PeriodHeader/PeriodHeader.tsx new file mode 100644 index 00000000000..fe8f3841591 --- /dev/null +++ b/apps/mobile/screens/(authenticated)/(home)/home/components/PeriodHeader/PeriodHeader.tsx @@ -0,0 +1,15 @@ +import { View } from "react-native"; +import { Text } from "@/components/ui/text"; + +export function PeriodHeader({ label }: { label: string }) { + return ( + + + {label} + + + ); +} diff --git a/apps/mobile/screens/(authenticated)/(home)/home/components/PeriodHeader/index.ts b/apps/mobile/screens/(authenticated)/(home)/home/components/PeriodHeader/index.ts new file mode 100644 index 00000000000..5aa85fe7d08 --- /dev/null +++ b/apps/mobile/screens/(authenticated)/(home)/home/components/PeriodHeader/index.ts @@ -0,0 +1 @@ +export { PeriodHeader } from "./PeriodHeader"; diff --git a/apps/mobile/screens/(authenticated)/(home)/home/components/ScopeBar/ScopeBar.tsx b/apps/mobile/screens/(authenticated)/(home)/home/components/ScopeBar/ScopeBar.tsx index 6f03169855a..c44412f1cb1 100644 --- a/apps/mobile/screens/(authenticated)/(home)/home/components/ScopeBar/ScopeBar.tsx +++ b/apps/mobile/screens/(authenticated)/(home)/home/components/ScopeBar/ScopeBar.tsx @@ -1,15 +1,16 @@ -import { View } from "react-native"; +import { ScrollView } from "react-native"; import { Icon } from "@/components/ui/icon"; import type { WorkspaceScope } from "@/screens/(authenticated)/(home)/home/stores/workspacesFilterStore"; import { CloudIcon } from "@/screens/(authenticated)/components/CloudIcon"; import { HostStatusDot } from "@/screens/(authenticated)/components/HostStatusDot"; import { Chip } from "./components/Chip"; +import { CloudFilterChips } from "./components/CloudFilterChips"; interface ScopeBarProps { scope: WorkspaceScope; hostName: string | null; hostOnline: boolean; - sortLabel: string; + sortLabel: string | null; onPressScope: () => void; onPressSort: () => void; } @@ -35,7 +36,11 @@ export function ScopeBar({ }: ScopeBarProps) { const cloud = scope === "cloud"; return ( - + {cloud || hostName ? ( ) : null} - - + {cloud ? : null} + {sortLabel ? : null} + ); } diff --git a/apps/mobile/screens/(authenticated)/(home)/home/components/ScopeBar/components/CloudFilterChips/CloudFilterChips.tsx b/apps/mobile/screens/(authenticated)/(home)/home/components/ScopeBar/components/CloudFilterChips/CloudFilterChips.tsx new file mode 100644 index 00000000000..e9df7d55e64 --- /dev/null +++ b/apps/mobile/screens/(authenticated)/(home)/home/components/ScopeBar/components/CloudFilterChips/CloudFilterChips.tsx @@ -0,0 +1,62 @@ +import { useLingui } from "@lingui/react/macro"; +import * as Haptics from "expo-haptics"; +import { useRouter } from "expo-router"; +import { useSession } from "@/lib/auth/client"; +import { useWorkspacesFilterStore } from "@/screens/(authenticated)/(home)/home/stores/workspacesFilterStore"; +import { UserAvatar } from "@/screens/(authenticated)/settings/components/UserAvatar"; +import { Chip } from "../Chip"; + +export function CloudFilterChips() { + const { t } = useLingui(); + const router = useRouter(); + const { data: session } = useSession(); + const status = useWorkspacesFilterStore((store) => store.cloudStatus); + const creator = useWorkspacesFilterStore((store) => store.cloudCreator); + + const person = + creator === "me" + ? { + label: t({ message: "Mine", context: "workspaces I created" }), + name: session?.user?.name ?? "", + image: session?.user?.image ?? null, + } + : creator === "anyone" + ? null + : { + label: creator.name.split(" ")[0], + name: creator.name, + image: creator.image, + }; + + const open = (path: "status" | "creator") => { + void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + router.push(`/(authenticated)/(home)/filter/${path}`); + }; + + return ( + <> + open("status")} + /> + + ) : undefined + } + onPress={() => open("creator")} + /> + + ); +} diff --git a/apps/mobile/screens/(authenticated)/(home)/home/components/ScopeBar/components/CloudFilterChips/index.ts b/apps/mobile/screens/(authenticated)/(home)/home/components/ScopeBar/components/CloudFilterChips/index.ts new file mode 100644 index 00000000000..9c149e04ae1 --- /dev/null +++ b/apps/mobile/screens/(authenticated)/(home)/home/components/ScopeBar/components/CloudFilterChips/index.ts @@ -0,0 +1 @@ +export { CloudFilterChips } from "./CloudFilterChips"; diff --git a/apps/mobile/screens/(authenticated)/(home)/home/components/WorkspaceRow/WorkspaceRow.tsx b/apps/mobile/screens/(authenticated)/(home)/home/components/WorkspaceRow/WorkspaceRow.tsx index 4b43ce09365..be1e7d1a0dd 100644 --- a/apps/mobile/screens/(authenticated)/(home)/home/components/WorkspaceRow/WorkspaceRow.tsx +++ b/apps/mobile/screens/(authenticated)/(home)/home/components/WorkspaceRow/WorkspaceRow.tsx @@ -84,6 +84,7 @@ export function WorkspaceRow({ // a host is deletable. canRename={cloudStatus === undefined || cloudStatus === "ready"} canDelete={cloudStatus === undefined || cloudStatus !== "provisioning"} + isCloud={cloudStatus !== undefined} isUnread={isUnread} onToggleUnread={toggleUnread} pinned={pinned} diff --git a/apps/mobile/screens/(authenticated)/(home)/home/components/WorkspaceRow/components/WorkspaceRowMenu/WorkspaceRowMenu.tsx b/apps/mobile/screens/(authenticated)/(home)/home/components/WorkspaceRow/components/WorkspaceRowMenu/WorkspaceRowMenu.tsx index 4ed91edb94c..73f86f3f8a9 100644 --- a/apps/mobile/screens/(authenticated)/(home)/home/components/WorkspaceRow/components/WorkspaceRowMenu/WorkspaceRowMenu.tsx +++ b/apps/mobile/screens/(authenticated)/(home)/home/components/WorkspaceRow/components/WorkspaceRowMenu/WorkspaceRowMenu.tsx @@ -7,6 +7,7 @@ export function WorkspaceRowMenu({ onTogglePin, canRename, canDelete, + isCloud, isUnread, onToggleUnread, onRename, @@ -16,9 +17,11 @@ export function WorkspaceRowMenu({ children, }: { pinned: boolean; - onTogglePin: () => void; + onTogglePin?: () => void; canRename: boolean; canDelete: boolean; + /** A cloud workspace is archived, not deleted. */ + isCloud: boolean; isUnread: boolean; onToggleUnread: () => void; onRename: () => void; @@ -52,18 +55,25 @@ export function WorkspaceRowMenu({ message: "Mark as Unread", })} - - {pinned ? t({ message: "Unpin" }) : t({ message: "Pin" })} - + {onTogglePin ? ( + + {pinned ? t({ message: "Unpin" }) : t({ message: "Pin" })} + + ) : null} {canRename ? ( {t({ message: "Rename" })} ) : null} - {canDelete ? ( + {canDelete && isCloud ? ( + + {t({ message: "Archive" })} + + ) : null} + {canDelete && !isCloud ? ( {t({ message: "Delete" })} diff --git a/apps/mobile/screens/(authenticated)/(home)/home/components/WorkspaceRow/hooks/useWorkspaceRowActions/useWorkspaceRowActions.ts b/apps/mobile/screens/(authenticated)/(home)/home/components/WorkspaceRow/hooks/useWorkspaceRowActions/useWorkspaceRowActions.ts index 6890f92b1e9..69059b888b2 100644 --- a/apps/mobile/screens/(authenticated)/(home)/home/components/WorkspaceRow/hooks/useWorkspaceRowActions/useWorkspaceRowActions.ts +++ b/apps/mobile/screens/(authenticated)/(home)/home/components/WorkspaceRow/hooks/useWorkspaceRowActions/useWorkspaceRowActions.ts @@ -103,15 +103,21 @@ export function useWorkspaceRowActions( cache.invalidateHost(workspace.hostId); }; - const deleteWorkspace = () => + const deleteWorkspace = () => { + if (isCloud) { + void cloud + .archive(workspace.id) + .catch(() => Alert.alert(t({ message: "Archive failed" }))); + return; + } remove({ id: workspace.id, name: workspace.name, type: workspace.type, hostId: workspace.hostId, hostUrl: cache.resolveHostUrl(workspace.hostId), - isCloud, }); + }; const copyId = () => void Clipboard.setStringAsync(workspace.id).then(onCopied); diff --git a/apps/mobile/screens/(authenticated)/(home)/home/hooks/useCloudPullRequests/index.ts b/apps/mobile/screens/(authenticated)/(home)/home/hooks/useCloudPullRequests/index.ts new file mode 100644 index 00000000000..f0b5bc1c4fe --- /dev/null +++ b/apps/mobile/screens/(authenticated)/(home)/home/hooks/useCloudPullRequests/index.ts @@ -0,0 +1,5 @@ +export { + type CloudPullRequest, + cloudPullRequestRefKey, + useCloudPullRequests, +} from "./useCloudPullRequests"; diff --git a/apps/mobile/screens/(authenticated)/(home)/home/hooks/useCloudPullRequests/useCloudPullRequests.ts b/apps/mobile/screens/(authenticated)/(home)/home/hooks/useCloudPullRequests/useCloudPullRequests.ts new file mode 100644 index 00000000000..68b430390f0 --- /dev/null +++ b/apps/mobile/screens/(authenticated)/(home)/home/hooks/useCloudPullRequests/useCloudPullRequests.ts @@ -0,0 +1,70 @@ +import type { RouterOutputs } from "@superset/trpc"; +import { useQuery } from "@tanstack/react-query"; +import { useMemo } from "react"; +import { useSession } from "@/lib/auth/client"; +import { apiClient } from "@/lib/trpc/client"; + +const REFETCH_INTERVAL_MS = 30_000; +const MAX_REFS_PER_QUERY = 500; + +export type CloudPullRequest = + RouterOutputs["integration"]["github"]["getByBranches"]["pullRequests"][number]; + +export interface CloudPullRequestRef { + repoFullName: string; + headBranch: string; +} + +export function cloudPullRequestRefKey(ref: CloudPullRequestRef): string { + return `${ref.repoFullName.toLowerCase()}\n${ref.headBranch}`; +} + +/** + * Desktop's useCloudPullRequests: each cloud workspace's pull request, read + * from the GitHub integration by repository and branch rather than from the + * sandbox, so a list of them wakes nothing. + */ +export function useCloudPullRequests( + refs: CloudPullRequestRef[], +): Map { + const { data: session } = useSession(); + const organizationId = session?.session?.activeOrganizationId ?? null; + + const stableRefs = useMemo(() => { + const byKey = new Map(); + for (const ref of refs) byKey.set(cloudPullRequestRefKey(ref), ref); + return [...byKey.entries()] + .sort(([left], [right]) => left.localeCompare(right)) + .map(([, ref]) => ref) + .slice(0, MAX_REFS_PER_QUERY); + }, [refs]); + + const { data } = useQuery({ + queryKey: [ + "cloud", + "integration", + "github", + "getByBranches", + organizationId, + stableRefs, + ], + enabled: organizationId !== null && stableRefs.length > 0, + refetchInterval: REFETCH_INTERVAL_MS, + staleTime: 10_000, + placeholderData: (previous, previousQuery) => + previousQuery?.queryKey[4] === organizationId ? previous : undefined, + queryFn: () => + apiClient.integration.github.getByBranches.query({ + organizationId: organizationId as string, + refs: stableRefs, + }), + }); + + return useMemo(() => { + const byRef = new Map(); + for (const pullRequest of data?.pullRequests ?? []) { + byRef.set(cloudPullRequestRefKey(pullRequest), pullRequest); + } + return byRef; + }, [data]); +} diff --git a/apps/mobile/screens/(authenticated)/(home)/home/hooks/useCloudRepoPrefixes/index.ts b/apps/mobile/screens/(authenticated)/(home)/home/hooks/useCloudRepoPrefixes/index.ts deleted file mode 100644 index 073eb8ebdcf..00000000000 --- a/apps/mobile/screens/(authenticated)/(home)/home/hooks/useCloudRepoPrefixes/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { useCloudRepoPrefixes } from "./useCloudRepoPrefixes"; diff --git a/apps/mobile/screens/(authenticated)/(home)/home/hooks/useCloudRepos/index.ts b/apps/mobile/screens/(authenticated)/(home)/home/hooks/useCloudRepos/index.ts new file mode 100644 index 00000000000..b661d19f964 --- /dev/null +++ b/apps/mobile/screens/(authenticated)/(home)/home/hooks/useCloudRepos/index.ts @@ -0,0 +1 @@ +export { useCloudRepos } from "./useCloudRepos"; diff --git a/apps/mobile/screens/(authenticated)/(home)/home/hooks/useCloudRepoPrefixes/useCloudRepoPrefixes.ts b/apps/mobile/screens/(authenticated)/(home)/home/hooks/useCloudRepos/useCloudRepos.ts similarity index 62% rename from apps/mobile/screens/(authenticated)/(home)/home/hooks/useCloudRepoPrefixes/useCloudRepoPrefixes.ts rename to apps/mobile/screens/(authenticated)/(home)/home/hooks/useCloudRepos/useCloudRepos.ts index 7df9ed9e26b..bba84e82076 100644 --- a/apps/mobile/screens/(authenticated)/(home)/home/hooks/useCloudRepoPrefixes/useCloudRepoPrefixes.ts +++ b/apps/mobile/screens/(authenticated)/(home)/home/hooks/useCloudRepos/useCloudRepos.ts @@ -5,11 +5,8 @@ import { useMemo } from "react"; import { useSession } from "@/lib/auth/client"; import { apiClient } from "@/lib/trpc/client"; -/** - * The repo URL prefix each cloud workspace's pull requests live under, by - * cloud workspace id: the primary repository it checked out. - */ -export function useCloudRepoPrefixes(): Map { +/** Each cloud workspace's primary repository, `owner/name`, by cloud workspace id. */ +export function useCloudRepos(): Map { const { data: session } = useSession(); const organizationId = session?.session?.activeOrganizationId ?? null; const enabledByFlag = Boolean(useFeatureFlag(FEATURE_FLAGS.CLOUD_WORKSPACES)); @@ -23,15 +20,13 @@ export function useCloudRepoPrefixes(): Map { organizationId: organizationId as string, }), }); - return useMemo(() => { - const prefixes = new Map(); - for (const row of data ?? []) { - if (!row.primary) continue; - prefixes.set( - row.cloudWorkspaceId, - `https://github.com/${row.fullName}/`.toLowerCase(), - ); - } - return prefixes; - }, [data]); + return useMemo( + () => + new Map( + (data ?? []) + .filter((row) => row.primary) + .map((row) => [row.cloudWorkspaceId, row.fullName]), + ), + [data], + ); } diff --git a/apps/mobile/screens/(authenticated)/(home)/home/hooks/useNow/index.ts b/apps/mobile/screens/(authenticated)/(home)/home/hooks/useNow/index.ts new file mode 100644 index 00000000000..49bab4a75f7 --- /dev/null +++ b/apps/mobile/screens/(authenticated)/(home)/home/hooks/useNow/index.ts @@ -0,0 +1 @@ +export { useNow } from "./useNow"; diff --git a/apps/mobile/screens/(authenticated)/(home)/home/hooks/useNow/useNow.ts b/apps/mobile/screens/(authenticated)/(home)/home/hooks/useNow/useNow.ts new file mode 100644 index 00000000000..0f38d13b8cf --- /dev/null +++ b/apps/mobile/screens/(authenticated)/(home)/home/hooks/useNow/useNow.ts @@ -0,0 +1,10 @@ +import { useEffect, useState } from "react"; + +export function useNow(intervalMs: number): Date { + const [now, setNow] = useState(() => new Date()); + useEffect(() => { + const interval = setInterval(() => setNow(new Date()), intervalMs); + return () => clearInterval(interval); + }, [intervalMs]); + return now; +} diff --git a/apps/mobile/screens/(authenticated)/(home)/home/stores/workspacesFilterStore/index.ts b/apps/mobile/screens/(authenticated)/(home)/home/stores/workspacesFilterStore/index.ts index bb901ddf963..2d7deb1550a 100644 --- a/apps/mobile/screens/(authenticated)/(home)/home/stores/workspacesFilterStore/index.ts +++ b/apps/mobile/screens/(authenticated)/(home)/home/stores/workspacesFilterStore/index.ts @@ -1,4 +1,6 @@ export { + type CloudCreatorFilter, + type CloudStatusFilter, SORT_OPTIONS, useWorkspacesFilterStore, type WorkspaceScope, diff --git a/apps/mobile/screens/(authenticated)/(home)/home/stores/workspacesFilterStore/workspacesFilterStore.ts b/apps/mobile/screens/(authenticated)/(home)/home/stores/workspacesFilterStore/workspacesFilterStore.ts index b00377a5780..341847c3831 100644 --- a/apps/mobile/screens/(authenticated)/(home)/home/stores/workspacesFilterStore/workspacesFilterStore.ts +++ b/apps/mobile/screens/(authenticated)/(home)/home/stores/workspacesFilterStore/workspacesFilterStore.ts @@ -9,6 +9,13 @@ export type WorkspaceSort = "updatedAt" | "createdAt"; /** Cloud is a place you scope to, not a machine you own. */ export type WorkspaceScope = "cloud" | "host"; +export type CloudStatusFilter = "active" | "archived"; + +export type CloudCreatorFilter = + | "me" + | "anyone" + | { userId: string; name: string; image: string | null }; + export const SORT_OPTIONS: { value: WorkspaceSort; label: MessageDescriptor; @@ -27,11 +34,15 @@ interface WorkspacesFilterStore { hostFilter: string | null; scope: WorkspaceScope; sort: WorkspaceSort; + cloudStatus: CloudStatusFilter; + cloudCreator: CloudCreatorFilter; /** False until AsyncStorage has answered — the saved filter isn't here yet. */ hasHydrated: boolean; setHostFilter: (machineId: string | null) => void; setScopeCloud: () => void; setSort: (sort: WorkspaceSort) => void; + setCloudStatus: (status: CloudStatusFilter) => void; + setCloudCreator: (creator: CloudCreatorFilter) => void; } export const useWorkspacesFilterStore = create()( @@ -40,6 +51,8 @@ export const useWorkspacesFilterStore = create()( hostFilter: null, scope: "host", sort: "updatedAt", + cloudStatus: "active", + cloudCreator: "me", hasHydrated: false, // Picking a machine is also how you leave Cloud; the machine is // remembered either way so Cloud → machine returns you where you were. @@ -47,14 +60,17 @@ export const useWorkspacesFilterStore = create()( set({ hostFilter: machineId, scope: "host" }), setScopeCloud: () => set({ scope: "cloud" }), setSort: (sort) => set({ sort }), + setCloudStatus: (cloudStatus) => set({ cloudStatus }), + setCloudCreator: (cloudCreator) => set({ cloudCreator }), }), { name: "workspaces-filter", storage: createJSONStorage(() => AsyncStorage), - partialize: ({ hostFilter, scope, sort }) => ({ + partialize: ({ hostFilter, scope, sort, cloudCreator }) => ({ hostFilter, scope, sort, + cloudCreator, }), // Rehydration is async — measured at ~165ms on a cold start — so // readers see the defaults first and the home screen would spend that diff --git a/apps/mobile/screens/(authenticated)/(home)/hooks/useCloudFilters/index.ts b/apps/mobile/screens/(authenticated)/(home)/hooks/useCloudFilters/index.ts new file mode 100644 index 00000000000..205b2f750f6 --- /dev/null +++ b/apps/mobile/screens/(authenticated)/(home)/hooks/useCloudFilters/index.ts @@ -0,0 +1 @@ +export { useCloudFilters } from "./useCloudFilters"; diff --git a/apps/mobile/screens/(authenticated)/(home)/hooks/useCloudFilters/useCloudFilters.ts b/apps/mobile/screens/(authenticated)/(home)/hooks/useCloudFilters/useCloudFilters.ts new file mode 100644 index 00000000000..c547e8fb312 --- /dev/null +++ b/apps/mobile/screens/(authenticated)/(home)/hooks/useCloudFilters/useCloudFilters.ts @@ -0,0 +1,21 @@ +import { useCallback } from "react"; +import { useSession } from "@/lib/auth/client"; +import { useWorkspacesFilterStore } from "@/screens/(authenticated)/(home)/home/stores/workspacesFilterStore"; + +export function useCloudFilters() { + const { data: session } = useSession(); + const status = useWorkspacesFilterStore((store) => store.cloudStatus); + const creator = useWorkspacesFilterStore((store) => store.cloudCreator); + const creatorId = + creator === "me" + ? (session?.user?.id ?? null) + : creator === "anyone" + ? null + : creator.userId; + const matchesCreator = useCallback( + (createdByUserId: string | null | undefined) => + creator === "anyone" || createdByUserId === creatorId, + [creator, creatorId], + ); + return { status, creator, matchesCreator }; +} diff --git a/apps/mobile/screens/(authenticated)/(home)/search/SearchScreen.tsx b/apps/mobile/screens/(authenticated)/(home)/search/SearchScreen.tsx index 586fce28fb8..b290a46b201 100644 --- a/apps/mobile/screens/(authenticated)/(home)/search/SearchScreen.tsx +++ b/apps/mobile/screens/(authenticated)/(home)/search/SearchScreen.tsx @@ -12,7 +12,11 @@ import { FlatList, Pressable, View } from "react-native"; import type { SearchBarCommands } from "react-native-screens"; import { Icon } from "@/components/ui/icon"; import { Text } from "@/components/ui/text"; -import { useCloudWorkspaceItems } from "@/hooks/useCloudWorkspaceItems"; +import { useArchivedCloudWorkspaces } from "@/hooks/useArchivedCloudWorkspaces"; +import { + itemFromCloudRow, + useCloudWorkspaceItems, +} from "@/hooks/useCloudWorkspaceItems"; import { useHostProjects } from "@/hooks/useHostProjects"; import { type HostWorkspaceItem, @@ -24,6 +28,7 @@ import { useHostsTerminals, } from "@/screens/(authenticated)/(home)/home/hooks/useHostTerminals"; import { useWorkspacesFilterStore } from "@/screens/(authenticated)/(home)/home/stores/workspacesFilterStore"; +import { useCloudFilters } from "@/screens/(authenticated)/(home)/hooks/useCloudFilters"; import { useSelectedHost } from "@/screens/(authenticated)/(home)/hooks/useSelectedHost"; import { useWorkspaceScope } from "@/screens/(authenticated)/(home)/hooks/useWorkspaceScope"; import { usePinnedWorkspacesStore } from "@/screens/(authenticated)/stores/pinnedWorkspacesStore"; @@ -53,6 +58,11 @@ export function SearchScreen() { const cloudScope = useWorkspaceScope() === "cloud"; const { workspaces } = useHostWorkspaces(selectedHost); const { items: cloudItems } = useCloudWorkspaceItems(); + const cloudFilters = useCloudFilters(); + const searchArchived = cloudScope && cloudFilters.status === "archived"; + const { workspaces: archivedCloud } = useArchivedCloudWorkspaces({ + enabled: searchArchived, + }); const { projects } = useHostProjects(selectedHost); const pinnedAt = usePinnedWorkspacesStore((state) => state.pinnedAt); @@ -119,7 +129,12 @@ export function SearchScreen() { const results = useMemo(() => { const pool = cloudScope - ? cloudItems + ? (searchArchived + ? archivedCloud.map(itemFromCloudRow) + : cloudItems + ).filter((workspace) => + cloudFilters.matchesCreator(workspace.createdByUserId), + ) : workspaces.filter( (workspace) => workspace.worktreeExists !== false && @@ -129,6 +144,9 @@ export function SearchScreen() { }, [ cloudScope, cloudItems, + searchArchived, + archivedCloud, + cloudFilters.matchesCreator, workspaces, selectedHost, matchesQuery, diff --git a/apps/mobile/screens/(authenticated)/components/WaveSpinner/WaveSpinner.tsx b/apps/mobile/screens/(authenticated)/components/WaveSpinner/WaveSpinner.tsx new file mode 100644 index 00000000000..1f7e47bdbf9 --- /dev/null +++ b/apps/mobile/screens/(authenticated)/components/WaveSpinner/WaveSpinner.tsx @@ -0,0 +1,41 @@ +import { useEffect } from "react"; +import { View } from "react-native"; +import { + Easing, + useSharedValue, + withRepeat, + withTiming, +} from "react-native-reanimated"; +import { FRAMES_PER_LOOP, LOOP_MS, WaveCell } from "./components/WaveCell"; + +const CELLS = Array.from({ length: 9 }, (_, index) => ({ + id: index, + diagonal: Math.floor(index / 3) + (index % 3), +})); + +export function WaveSpinner({ color }: { color: string }) { + const frame = useSharedValue(0); + useEffect(() => { + frame.value = withRepeat( + withTiming(FRAMES_PER_LOOP, { duration: LOOP_MS, easing: Easing.linear }), + -1, + false, + ); + }, [frame]); + + return ( + + {CELLS.map((cell) => ( + + ))} + + ); +} diff --git a/apps/mobile/screens/(authenticated)/components/WaveSpinner/components/WaveCell/WaveCell.tsx b/apps/mobile/screens/(authenticated)/components/WaveSpinner/components/WaveCell/WaveCell.tsx new file mode 100644 index 00000000000..c42c42ab78b --- /dev/null +++ b/apps/mobile/screens/(authenticated)/components/WaveSpinner/components/WaveCell/WaveCell.tsx @@ -0,0 +1,38 @@ +import Animated, { + type SharedValue, + useAnimatedStyle, +} from "react-native-reanimated"; + +export const FRAMES_PER_LOOP = 24; +export const LOOP_MS = FRAMES_PER_LOOP * 50; +const FADE_FRAMES = 3; +const DIAGONAL_DELAY_FRAMES = 2; +const EMPTY_START_FRAME = 11; + +export function WaveCell({ + frame, + diagonal, + color, +}: { + frame: SharedValue; + diagonal: number; + color: string; +}) { + const style = useAnimatedStyle(() => { + const ramp = (frames: number) => + Math.min(1, Math.max(0, frames / FADE_FRAMES)); + const step = Math.floor(frame.value); + const delay = diagonal * DIAGONAL_DELAY_FRAMES; + return { + opacity: ramp(step - delay) - ramp(step - EMPTY_START_FRAME - delay), + }; + }); + return ( + + ); +} diff --git a/apps/mobile/screens/(authenticated)/components/WaveSpinner/components/WaveCell/index.ts b/apps/mobile/screens/(authenticated)/components/WaveSpinner/components/WaveCell/index.ts new file mode 100644 index 00000000000..1d72fcd4f4d --- /dev/null +++ b/apps/mobile/screens/(authenticated)/components/WaveSpinner/components/WaveCell/index.ts @@ -0,0 +1 @@ +export { FRAMES_PER_LOOP, LOOP_MS, WaveCell } from "./WaveCell"; diff --git a/apps/mobile/screens/(authenticated)/components/WaveSpinner/index.ts b/apps/mobile/screens/(authenticated)/components/WaveSpinner/index.ts new file mode 100644 index 00000000000..4272718f05c --- /dev/null +++ b/apps/mobile/screens/(authenticated)/components/WaveSpinner/index.ts @@ -0,0 +1 @@ +export { WaveSpinner } from "./WaveSpinner"; diff --git a/apps/mobile/screens/(authenticated)/stores/unreadWorkspacesStore/unreadWorkspacesStore.ts b/apps/mobile/screens/(authenticated)/stores/unreadWorkspacesStore/unreadWorkspacesStore.ts index 28c8b79cbae..8aca8f0e908 100644 --- a/apps/mobile/screens/(authenticated)/stores/unreadWorkspacesStore/unreadWorkspacesStore.ts +++ b/apps/mobile/screens/(authenticated)/stores/unreadWorkspacesStore/unreadWorkspacesStore.ts @@ -7,8 +7,13 @@ import { createJSONStorage, persist } from "zustand/middleware"; * manualUnread: a marked workspace wears `review` attention on home until it * is opened or marked read again. */ +const MAX_CLOUD_READS = 300; + interface UnreadWorkspacesStore { manualUnread: Record; + /** Desktop's cloud lastReadAt: the agent update a cloud workspace was last opened at. */ + cloudReadAt: Record; + markCloudRead: (workspaceId: string, agentStatusAt: number) => void; setManualUnread: (workspaceId: string) => void; clearManualUnread: (workspaceId: string) => void; } @@ -17,6 +22,21 @@ export const useUnreadWorkspacesStore = create()( persist( (set) => ({ manualUnread: {}, + cloudReadAt: {}, + markCloudRead: (workspaceId, agentStatusAt) => { + set((state) => { + if ((state.cloudReadAt[workspaceId] ?? 0) >= agentStatusAt) { + return state; + } + const entries = Object.entries({ + ...state.cloudReadAt, + [workspaceId]: agentStatusAt, + }) + .sort((left, right) => right[1] - left[1]) + .slice(0, MAX_CLOUD_READS); + return { cloudReadAt: Object.fromEntries(entries) }; + }); + }, setManualUnread: (workspaceId) => { set((state) => ({ manualUnread: { ...state.manualUnread, [workspaceId]: true }, diff --git a/apps/mobile/screens/(authenticated)/workspace/[id]/WorkspaceScreen/WorkspaceScreen.tsx b/apps/mobile/screens/(authenticated)/workspace/[id]/WorkspaceScreen/WorkspaceScreen.tsx index deac7f2b2d0..a9aaeb373e3 100644 --- a/apps/mobile/screens/(authenticated)/workspace/[id]/WorkspaceScreen/WorkspaceScreen.tsx +++ b/apps/mobile/screens/(authenticated)/workspace/[id]/WorkspaceScreen/WorkspaceScreen.tsx @@ -29,6 +29,7 @@ import { import { useSafeAreaInsets } from "react-native-safe-area-context"; import { Icon } from "@/components/ui/icon"; import { Text } from "@/components/ui/text"; +import { useArchivedCloudWorkspaces } from "@/hooks/useArchivedCloudWorkspaces"; import { getHostWorkspacesQueryKey } from "@/hooks/useHostWorkspaces"; import { useWorkspaceHost } from "@/hooks/useWorkspaceHost"; import { errorCopy } from "@/lib/errors"; @@ -136,6 +137,12 @@ export function WorkspaceScreen() { retrySandbox, isResolving, } = useWorkspaceHost(id ?? null); + const { workspaces: archivedRows } = useArchivedCloudWorkspaces({ + enabled: !cloud && !workspace && !isResolving, + }); + const archivedCloud = cloud + ? null + : (archivedRows.find((row) => row.id === id) ?? null); const { terminalsByWorkspace, isReady: terminalsReady, @@ -433,6 +440,14 @@ export function WorkspaceScreen() { useEffect(() => { if (id) clearManualUnread(id); }, [id, clearManualUnread]); + const markCloudRead = useUnreadWorkspacesStore( + (state) => state.markCloudRead, + ); + const cloudAgentStatusAt = cloud?.agentStatusAt?.getTime() ?? null; + useEffect(() => { + if (id && cloudAgentStatusAt !== null) + markCloudRead(id, cloudAgentStatusAt); + }, [id, cloudAgentStatusAt, markCloudRead]); // Port of desktop's useClearActivePaneAttention: viewing the tab clears // its `review` state by advancing the seen mark to the binding's last @@ -863,7 +878,7 @@ export function WorkspaceScreen() { ( - - {t({ message: "Delete workspace" })} - + {cloud ? ( + + {t({ message: "Archive workspace" })} + + ) : ( + + {t({ message: "Delete workspace" })} + + )} @@ -1031,6 +1055,12 @@ export function WorkspaceScreen() { }} /> + ) : archivedCloud ? ( + ) : cloud && !host ? ( ; + } if (cloud.status === "failed") { return ; } @@ -100,10 +105,43 @@ export function CloudWorkspaceProvisioningState({ ); } +function CloudWorkspaceArchivedState({ cloud }: { cloud: CloudWorkspaceRow }) { + const { t } = useLingui(); + const { formatCompactRelativeTime } = useFormat(); + const { unarchive } = useCloudWorkspaceActions(); + + return ( + + + + + ); +} + /** * Provisioning gave up. The row is all that is left of the workspace — the * sandbox behind it was torn down when it failed — so the only thing to offer - * is disposing of it, which is also the only way to clear it from the list. + * is archiving it, which is also the only way to clear it from the list. */ function CloudWorkspaceFailedState({ cloud }: { cloud: CloudWorkspaceRow }) { const { t } = useLingui(); @@ -118,9 +156,9 @@ function CloudWorkspaceFailedState({ cloud }: { cloud: CloudWorkspaceRow }) { /> - Nothing is running. Remove it and create a new one. + Nothing is running. Archive it and create a new one. - + ); } @@ -147,7 +185,7 @@ function CloudWorkspaceUnreachableState({ - Still trying. If it keeps failing, remove it and create a new one. + Still trying. If it keeps failing, archive it and create a new one. @@ -156,37 +194,27 @@ function CloudWorkspaceUnreachableState({ Try again - + ); } -function RemoveWorkspaceButton({ workspaceId }: { workspaceId: string }) { +function ArchiveWorkspaceButton({ workspaceId }: { workspaceId: string }) { const { t } = useLingui(); - const router = useRouter(); - const { remove: removeCloudWorkspace } = useCloudWorkspaceActions(); - const [isDeleting, setIsDeleting] = useState(false); - - const remove = async () => { - setIsDeleting(true); - try { - await removeCloudWorkspace(workspaceId); - router.back(); - } catch { - Alert.alert(t({ message: "Delete failed" })); - setIsDeleting(false); - } - }; + const { archive } = useCloudWorkspaceActions(); return ( - ); diff --git a/apps/mobile/screens/(authenticated)/workspace/[id]/hooks/useWorkspaceHeaderActions/useWorkspaceHeaderActions.ts b/apps/mobile/screens/(authenticated)/workspace/[id]/hooks/useWorkspaceHeaderActions/useWorkspaceHeaderActions.ts index 51a24aa3d97..2f5b33e1cbc 100644 --- a/apps/mobile/screens/(authenticated)/workspace/[id]/hooks/useWorkspaceHeaderActions/useWorkspaceHeaderActions.ts +++ b/apps/mobile/screens/(authenticated)/workspace/[id]/hooks/useWorkspaceHeaderActions/useWorkspaceHeaderActions.ts @@ -68,6 +68,13 @@ export function useWorkspaceHeaderActions( const deleteWorkspace = () => { if (!workspace) return; + if (isCloud) { + // The screen stays: it turns into the archived state, with Unarchive. + void cloud + .archive(workspace.id) + .catch(() => Alert.alert(t({ message: "Archive failed" }))); + return; + } if (!host) { Alert.alert( t({ @@ -83,7 +90,6 @@ export function useWorkspaceHeaderActions( type: workspace.type, hostId: host.machineId, hostUrl: hostServiceUrl(host.organizationId, host.machineId), - isCloud, }, // Nothing on this screen outlives the workspace: every panel below // reads a row that is now gone, and the host placeholder it falls diff --git a/apps/mobile/screens/(authenticated)/workspace/[id]/hooks/useWorkspacePullRequestDetail/useWorkspacePullRequestDetail.ts b/apps/mobile/screens/(authenticated)/workspace/[id]/hooks/useWorkspacePullRequestDetail/useWorkspacePullRequestDetail.ts index eb2acb7dd4c..39a09f75385 100644 --- a/apps/mobile/screens/(authenticated)/workspace/[id]/hooks/useWorkspacePullRequestDetail/useWorkspacePullRequestDetail.ts +++ b/apps/mobile/screens/(authenticated)/workspace/[id]/hooks/useWorkspacePullRequestDetail/useWorkspacePullRequestDetail.ts @@ -36,7 +36,8 @@ export function useWorkspacePullRequestDetail({ repo: string | null; pullNumber: number | null; }) { - const { host } = useWorkspaceHost(workspaceId); + const { host, isResolving, sandboxWaking, sandboxUnreachable } = + useWorkspaceHost(workspaceId); const hostUrl = host?.isOnline === true ? hostServiceUrl(host.organizationId, host.machineId) @@ -65,7 +66,11 @@ export function useWorkspacePullRequestDetail({ return { detail: query.data ?? null, - isLoading: ready && query.isPending, + // An idle query is not an empty one: while the box is still waking the + // pull request is unknown, not gone. + isLoading: ready + ? query.isPending + : (isResolving || sandboxWaking) && !sandboxUnreachable, isRefetching: query.isRefetching, error: query.error, refetch: query.refetch, diff --git a/packages/i18n/locales/cs/messages.po b/packages/i18n/locales/cs/messages.po index f34891a61a3..8fe19f86f20 100644 --- a/packages/i18n/locales/cs/messages.po +++ b/packages/i18n/locales/cs/messages.po @@ -1526,6 +1526,10 @@ msgstr "Aktivace, kolik toho lidé používají, když už jsou uvnitř, a jestl msgid "Active" msgstr "Aktivní" +msgctxt "cloud workspace status" +msgid "Active" +msgstr "Aktivní" + msgctxt "presence" msgid "Active" msgstr "Aktivní" @@ -2316,6 +2320,9 @@ msgstr "Archivovat" msgid "Archive environment" msgstr "Archivovat prostředí" +msgid "Archive failed" +msgstr "Archivace selhala" + msgid "Archive workspace" msgstr "Archivovat pracovní prostor" @@ -2323,6 +2330,7 @@ msgid "Archived" msgstr "Archivované" #. placeholder {0}: formatCompactRelativeTime(archivedAt, now) +#. placeholder {0}: formatCompactRelativeTime(cloud.deletedAt) msgid "Archived · {0}" msgstr "Archivováno · {0}" @@ -5046,6 +5054,9 @@ msgstr "Vytvořil(a)" msgid "Created by <0/> · {createdAgo}" msgstr "Vytvořil(a) <0/> · {createdAgo}" +msgid "Created by me" +msgstr "Vytvořeno mnou" + msgid "Created team \"{trimmedName}\"" msgstr "Tým „{trimmedName}“ vytvořen" @@ -5296,10 +5307,6 @@ msgstr "Smazat" msgid "Delete \"{0}\"? This removes its worktree from the host." msgstr "Smazat „{0}“? Tím se z hosta odstraní jeho worktree." -#. placeholder {0}: target.name -msgid "Delete \"{0}\"? This shuts down its sandbox and everything in it." -msgstr "Smazat „{0}“? Tím se vypne jeho sandbox i všechno v něm." - msgid "Delete \"{basename}\"?" msgstr "Smazat „{basename}“?" @@ -5347,9 +5354,6 @@ msgstr "Smazat automatizaci" msgid "Delete automation?" msgstr "Smazat automatizaci?" -msgid "Delete cloud workspace" -msgstr "Smazat cloudový pracovní prostor" - #. placeholder {0}: index + 1 msgid "Delete command {0}" msgstr "Smazat příkaz {0}" @@ -9382,6 +9386,10 @@ msgstr "Pol. 2027" msgid "Mine" msgstr "Moje" +msgctxt "workspaces I created" +msgid "Mine" +msgstr "Moje" + msgid "Minimal reasoning effort" msgstr "Minimální uvažování" @@ -9934,6 +9942,9 @@ msgstr "Zatím žádní agenti." msgid "No API keys yet." msgstr "Zatím žádné API klíče." +msgid "No archived workspaces" +msgstr "Žádné archivované pracovní prostory" + msgid "No assignee" msgstr "Bez řešitele" @@ -9982,6 +9993,9 @@ msgstr "Nebyl nalezen Chrome, Brave, Arc ani jiný prohlížeč založený na Ch msgid "No Claude or Codex usage found on this machine yet. Join now and you'll appear once you've used an agent." msgstr "Na tomto počítači zatím nebylo nalezeno žádné využití Claude ani Codex. Připojte se a objevíte se, jakmile použijete agenta." +msgid "No cloud workspaces match these filters" +msgstr "Těmto filtrům neodpovídají žádné cloudové pracovní prostory" + msgid "No cloud workspaces yet" msgstr "Zatím žádné cloudové pracovní prostory" @@ -10566,8 +10580,8 @@ msgstr "Nic tu není." msgid "Nothing installed yet." msgstr "Zatím není nic nainstalováno." -msgid "Nothing is running. Remove it and create a new one." -msgstr "Nic neběží. Odeberte ho a vytvořte nový." +msgid "Nothing is running. Archive it and create a new one." +msgstr "Nic neběží. Archivujte ho a vytvořte nový." msgid "Nothing lives at {pathname}." msgstr "Na {pathname} nic není." @@ -12242,6 +12256,7 @@ msgid "Pull request" msgstr "Pull request" #. placeholder {0}: data.number +#. placeholder {0}: pullRequest.number #. placeholder {0}: pullRequest.prNumber msgid "Pull request #{0}" msgstr "Pull request #{0}" @@ -15000,8 +15015,8 @@ msgstr "Krok {step}: {name}" msgid "Still can't reach the host." msgstr "Host je stále nedostupný." -msgid "Still trying. If it keeps failing, remove it and create a new one." -msgstr "Stále to zkoušíme. Pokud to dál selhává, odeberte ho a vytvořte nový." +msgid "Still trying. If it keeps failing, archive it and create a new one." +msgstr "Stále to zkoušíme. Pokud to dál selhává, archivujte ho a vytvořte nový." msgid "Still working — large repos can take a few minutes. You can leave; the workspace will appear on Home when it's ready." msgstr "Stále pracuji — u velkých repozitářů to může trvat pár minut. Můžete odejít; pracovní prostor se objeví na úvodní obrazovce, až bude hotový." @@ -16772,6 +16787,9 @@ msgstr "Zvuk se nepodařilo přepsat" msgid "Unarchive" msgstr "Obnovit z archivu" +msgid "Unarchive failed" +msgstr "Obnovení z archivu selhalo" + msgid "Unarchive workspace" msgstr "Obnovit pracovní prostor z archivu" diff --git a/packages/i18n/locales/de/messages.po b/packages/i18n/locales/de/messages.po index c780c4cb5d9..798a7563c2e 100644 --- a/packages/i18n/locales/de/messages.po +++ b/packages/i18n/locales/de/messages.po @@ -1526,6 +1526,10 @@ msgstr "Aktivierung, wie viel Menschen es nutzen, sobald sie drin sind, und ob s msgid "Active" msgstr "Aktiv" +msgctxt "cloud workspace status" +msgid "Active" +msgstr "Aktiv" + msgctxt "presence" msgid "Active" msgstr "Aktiv" @@ -2316,6 +2320,9 @@ msgstr "Archivieren" msgid "Archive environment" msgstr "Umgebung archivieren" +msgid "Archive failed" +msgstr "Archivieren fehlgeschlagen" + msgid "Archive workspace" msgstr "Arbeitsbereich archivieren" @@ -2323,6 +2330,7 @@ msgid "Archived" msgstr "Archiviert" #. placeholder {0}: formatCompactRelativeTime(archivedAt, now) +#. placeholder {0}: formatCompactRelativeTime(cloud.deletedAt) msgid "Archived · {0}" msgstr "Archiviert · {0}" @@ -5046,6 +5054,9 @@ msgstr "Erstellt von" msgid "Created by <0/> · {createdAgo}" msgstr "Erstellt von <0/> · {createdAgo}" +msgid "Created by me" +msgstr "Von mir erstellt" + msgid "Created team \"{trimmedName}\"" msgstr "Team \"{trimmedName}\" erstellt" @@ -5296,10 +5307,6 @@ msgstr "Löschen" msgid "Delete \"{0}\"? This removes its worktree from the host." msgstr "\"{0}\" löschen? Damit wird sein Worktree vom Host entfernt." -#. placeholder {0}: target.name -msgid "Delete \"{0}\"? This shuts down its sandbox and everything in it." -msgstr "\"{0}\" löschen? Damit werden seine Sandbox und alles darin heruntergefahren." - msgid "Delete \"{basename}\"?" msgstr "\"{basename}\" löschen?" @@ -5347,9 +5354,6 @@ msgstr "Automatisierung löschen" msgid "Delete automation?" msgstr "Automatisierung löschen?" -msgid "Delete cloud workspace" -msgstr "Cloud-Arbeitsbereich löschen" - #. placeholder {0}: index + 1 msgid "Delete command {0}" msgstr "Befehl {0} löschen" @@ -9382,6 +9386,10 @@ msgstr "Mitte 2027" msgid "Mine" msgstr "Meine" +msgctxt "workspaces I created" +msgid "Mine" +msgstr "Meine" + msgid "Minimal reasoning effort" msgstr "Minimales Reasoning" @@ -9934,6 +9942,9 @@ msgstr "Noch keine Agenten." msgid "No API keys yet." msgstr "Noch keine API-Schlüssel." +msgid "No archived workspaces" +msgstr "Keine archivierten Arbeitsbereiche" + msgid "No assignee" msgstr "Kein Zuständiger" @@ -9982,6 +9993,9 @@ msgstr "Es wurden keine Chromium-Browser wie Chrome, Brave oder Arc gefunden." msgid "No Claude or Codex usage found on this machine yet. Join now and you'll appear once you've used an agent." msgstr "Auf diesem Rechner wurde noch keine Claude- oder Codex-Nutzung gefunden. Tritt jetzt bei und du erscheinst, sobald du einen Agenten genutzt hast." +msgid "No cloud workspaces match these filters" +msgstr "Keine Cloud-Arbeitsbereiche entsprechen diesen Filtern" + msgid "No cloud workspaces yet" msgstr "Noch keine Cloud-Arbeitsbereiche" @@ -10566,8 +10580,8 @@ msgstr "Hier ist nichts." msgid "Nothing installed yet." msgstr "Noch nichts installiert." -msgid "Nothing is running. Remove it and create a new one." -msgstr "Es läuft nichts. Entferne ihn und erstelle einen neuen." +msgid "Nothing is running. Archive it and create a new one." +msgstr "Es läuft nichts. Archiviere ihn und erstelle einen neuen." msgid "Nothing lives at {pathname}." msgstr "Unter {pathname} liegt nichts." @@ -12242,6 +12256,7 @@ msgid "Pull request" msgstr "Pull Request" #. placeholder {0}: data.number +#. placeholder {0}: pullRequest.number #. placeholder {0}: pullRequest.prNumber msgid "Pull request #{0}" msgstr "Pull Request #{0}" @@ -15000,8 +15015,8 @@ msgstr "Schritt {step}: {name}" msgid "Still can't reach the host." msgstr "Der Host ist weiterhin nicht erreichbar." -msgid "Still trying. If it keeps failing, remove it and create a new one." -msgstr "Wird weiter versucht. Wenn es weiter fehlschlägt, entferne ihn und erstelle einen neuen." +msgid "Still trying. If it keeps failing, archive it and create a new one." +msgstr "Wird weiter versucht. Wenn es weiter fehlschlägt, archiviere ihn und erstelle einen neuen." msgid "Still working — large repos can take a few minutes. You can leave; the workspace will appear on Home when it's ready." msgstr "Läuft noch — große Repos können ein paar Minuten dauern. Du kannst weggehen; der Arbeitsbereich erscheint auf der Startseite, sobald er bereit ist." @@ -16772,6 +16787,9 @@ msgstr "Audio kann nicht transkribiert werden" msgid "Unarchive" msgstr "Wiederherstellen" +msgid "Unarchive failed" +msgstr "Wiederherstellen fehlgeschlagen" + msgid "Unarchive workspace" msgstr "Arbeitsbereich wiederherstellen" diff --git a/packages/i18n/locales/en/messages.po b/packages/i18n/locales/en/messages.po index 5d571f6551e..727b9043d21 100644 --- a/packages/i18n/locales/en/messages.po +++ b/packages/i18n/locales/en/messages.po @@ -1526,6 +1526,10 @@ msgstr "Activation, how much people use it once they are in, and whether they co msgid "Active" msgstr "Active" +msgctxt "cloud workspace status" +msgid "Active" +msgstr "Active" + msgctxt "presence" msgid "Active" msgstr "Active" @@ -2316,6 +2320,9 @@ msgstr "Archive" msgid "Archive environment" msgstr "Archive environment" +msgid "Archive failed" +msgstr "Archive failed" + msgid "Archive workspace" msgstr "Archive workspace" @@ -2323,6 +2330,7 @@ msgid "Archived" msgstr "Archived" #. placeholder {0}: formatCompactRelativeTime(archivedAt, now) +#. placeholder {0}: formatCompactRelativeTime(cloud.deletedAt) msgid "Archived · {0}" msgstr "Archived · {0}" @@ -5046,6 +5054,9 @@ msgstr "Created by" msgid "Created by <0/> · {createdAgo}" msgstr "Created by <0/> · {createdAgo}" +msgid "Created by me" +msgstr "Created by me" + msgid "Created team \"{trimmedName}\"" msgstr "Created team \"{trimmedName}\"" @@ -5296,10 +5307,6 @@ msgstr "Delete" msgid "Delete \"{0}\"? This removes its worktree from the host." msgstr "Delete \"{0}\"? This removes its worktree from the host." -#. placeholder {0}: target.name -msgid "Delete \"{0}\"? This shuts down its sandbox and everything in it." -msgstr "Delete \"{0}\"? This shuts down its sandbox and everything in it." - msgid "Delete \"{basename}\"?" msgstr "Delete \"{basename}\"?" @@ -5347,9 +5354,6 @@ msgstr "Delete automation" msgid "Delete automation?" msgstr "Delete automation?" -msgid "Delete cloud workspace" -msgstr "Delete cloud workspace" - #. placeholder {0}: index + 1 msgid "Delete command {0}" msgstr "Delete command {0}" @@ -9382,6 +9386,10 @@ msgstr "Mid 2027" msgid "Mine" msgstr "Mine" +msgctxt "workspaces I created" +msgid "Mine" +msgstr "Mine" + msgid "Minimal reasoning effort" msgstr "Minimal reasoning effort" @@ -9934,6 +9942,9 @@ msgstr "No agents yet." msgid "No API keys yet." msgstr "No API keys yet." +msgid "No archived workspaces" +msgstr "No archived workspaces" + msgid "No assignee" msgstr "No assignee" @@ -9982,6 +9993,9 @@ msgstr "No Chrome, Brave, Arc, or other Chromium browsers were found." msgid "No Claude or Codex usage found on this machine yet. Join now and you'll appear once you've used an agent." msgstr "No Claude or Codex usage found on this machine yet. Join now and you'll appear once you've used an agent." +msgid "No cloud workspaces match these filters" +msgstr "No cloud workspaces match these filters" + msgid "No cloud workspaces yet" msgstr "No cloud workspaces yet" @@ -10566,8 +10580,8 @@ msgstr "Nothing here." msgid "Nothing installed yet." msgstr "Nothing installed yet." -msgid "Nothing is running. Remove it and create a new one." -msgstr "Nothing is running. Remove it and create a new one." +msgid "Nothing is running. Archive it and create a new one." +msgstr "Nothing is running. Archive it and create a new one." msgid "Nothing lives at {pathname}." msgstr "Nothing lives at {pathname}." @@ -12242,6 +12256,7 @@ msgid "Pull request" msgstr "Pull request" #. placeholder {0}: data.number +#. placeholder {0}: pullRequest.number #. placeholder {0}: pullRequest.prNumber msgid "Pull request #{0}" msgstr "Pull request #{0}" @@ -15000,8 +15015,8 @@ msgstr "Step {step}: {name}" msgid "Still can't reach the host." msgstr "Still can't reach the host." -msgid "Still trying. If it keeps failing, remove it and create a new one." -msgstr "Still trying. If it keeps failing, remove it and create a new one." +msgid "Still trying. If it keeps failing, archive it and create a new one." +msgstr "Still trying. If it keeps failing, archive it and create a new one." msgid "Still working — large repos can take a few minutes. You can leave; the workspace will appear on Home when it's ready." msgstr "Still working — large repos can take a few minutes. You can leave; the workspace will appear on Home when it's ready." @@ -16772,6 +16787,9 @@ msgstr "Unable to transcribe audio" msgid "Unarchive" msgstr "Unarchive" +msgid "Unarchive failed" +msgstr "Unarchive failed" + msgid "Unarchive workspace" msgstr "Unarchive workspace" diff --git a/packages/i18n/locales/es/messages.po b/packages/i18n/locales/es/messages.po index 64f865dc8fb..695bed07a99 100644 --- a/packages/i18n/locales/es/messages.po +++ b/packages/i18n/locales/es/messages.po @@ -1526,6 +1526,10 @@ msgstr "La activación, cuánto lo usan una vez dentro y si vuelven." msgid "Active" msgstr "Activo" +msgctxt "cloud workspace status" +msgid "Active" +msgstr "Activos" + msgctxt "presence" msgid "Active" msgstr "Activo" @@ -2316,6 +2320,9 @@ msgstr "Archivar" msgid "Archive environment" msgstr "Archivar entorno" +msgid "Archive failed" +msgstr "No se pudo archivar" + msgid "Archive workspace" msgstr "Archivar espacio de trabajo" @@ -2323,6 +2330,7 @@ msgid "Archived" msgstr "Archivados" #. placeholder {0}: formatCompactRelativeTime(archivedAt, now) +#. placeholder {0}: formatCompactRelativeTime(cloud.deletedAt) msgid "Archived · {0}" msgstr "Archivado · {0}" @@ -5046,6 +5054,9 @@ msgstr "Creado por" msgid "Created by <0/> · {createdAgo}" msgstr "Creado por <0/> · {createdAgo}" +msgid "Created by me" +msgstr "Creados por mí" + msgid "Created team \"{trimmedName}\"" msgstr "Equipo \"{trimmedName}\" creado" @@ -5296,10 +5307,6 @@ msgstr "Eliminar" msgid "Delete \"{0}\"? This removes its worktree from the host." msgstr "¿Eliminar \"{0}\"? Esto quita su worktree del host." -#. placeholder {0}: target.name -msgid "Delete \"{0}\"? This shuts down its sandbox and everything in it." -msgstr "¿Eliminar \"{0}\"? Esto apaga su sandbox y todo lo que contiene." - msgid "Delete \"{basename}\"?" msgstr "¿Eliminar \"{basename}\"?" @@ -5347,9 +5354,6 @@ msgstr "Eliminar la automatización" msgid "Delete automation?" msgstr "¿Eliminar la automatización?" -msgid "Delete cloud workspace" -msgstr "Eliminar el espacio de trabajo en la nube" - #. placeholder {0}: index + 1 msgid "Delete command {0}" msgstr "Eliminar el comando {0}" @@ -9382,6 +9386,10 @@ msgstr "Mediados de 2027" msgid "Mine" msgstr "Mías" +msgctxt "workspaces I created" +msgid "Mine" +msgstr "Míos" + msgid "Minimal reasoning effort" msgstr "Esfuerzo de razonamiento mínimo" @@ -9934,6 +9942,9 @@ msgstr "Aún no hay agentes." msgid "No API keys yet." msgstr "Aún no hay claves de API." +msgid "No archived workspaces" +msgstr "No hay espacios de trabajo archivados" + msgid "No assignee" msgstr "Sin responsable" @@ -9982,6 +9993,9 @@ msgstr "No se encontraron Chrome, Brave, Arc ni otros navegadores basados en Chr msgid "No Claude or Codex usage found on this machine yet. Join now and you'll appear once you've used an agent." msgstr "Todavía no se ha encontrado uso de Claude ni de Codex en este equipo. Únete ahora y aparecerás en cuanto uses un agente." +msgid "No cloud workspaces match these filters" +msgstr "Ningún espacio de trabajo en la nube coincide con estos filtros" + msgid "No cloud workspaces yet" msgstr "Aún no hay espacios de trabajo en la nube" @@ -10566,8 +10580,8 @@ msgstr "Aquí no hay nada." msgid "Nothing installed yet." msgstr "Aún no hay nada instalado." -msgid "Nothing is running. Remove it and create a new one." -msgstr "No hay nada en ejecución. Quítalo y crea uno nuevo." +msgid "Nothing is running. Archive it and create a new one." +msgstr "No hay nada en ejecución. Archívalo y crea uno nuevo." msgid "Nothing lives at {pathname}." msgstr "No hay nada en {pathname}." @@ -12242,6 +12256,7 @@ msgid "Pull request" msgstr "Pull request" #. placeholder {0}: data.number +#. placeholder {0}: pullRequest.number #. placeholder {0}: pullRequest.prNumber msgid "Pull request #{0}" msgstr "Pull request #{0}" @@ -15000,8 +15015,8 @@ msgstr "Paso {step}: {name}" msgid "Still can't reach the host." msgstr "El host sigue sin responder." -msgid "Still trying. If it keeps failing, remove it and create a new one." -msgstr "Seguimos intentándolo. Si sigue fallando, quítalo y crea uno nuevo." +msgid "Still trying. If it keeps failing, archive it and create a new one." +msgstr "Seguimos intentándolo. Si sigue fallando, archívalo y crea uno nuevo." msgid "Still working — large repos can take a few minutes. You can leave; the workspace will appear on Home when it's ready." msgstr "Sigue en marcha: los repositorios grandes pueden tardar unos minutos. Puedes salir; el espacio de trabajo aparecerá en Inicio cuando esté listo." @@ -16772,6 +16787,9 @@ msgstr "No se pudo transcribir el audio" msgid "Unarchive" msgstr "Desarchivar" +msgid "Unarchive failed" +msgstr "No se pudo desarchivar" + msgid "Unarchive workspace" msgstr "Desarchivar espacio de trabajo" diff --git a/packages/i18n/locales/fr/messages.po b/packages/i18n/locales/fr/messages.po index be8aa76d098..32e4fbada00 100644 --- a/packages/i18n/locales/fr/messages.po +++ b/packages/i18n/locales/fr/messages.po @@ -1526,6 +1526,10 @@ msgstr "L'activation, l'usage une fois les gens entrés, et s'ils reviennent." msgid "Active" msgstr "Actives" +msgctxt "cloud workspace status" +msgid "Active" +msgstr "Actifs" + msgctxt "presence" msgid "Active" msgstr "Actif" @@ -2316,6 +2320,9 @@ msgstr "Archiver" msgid "Archive environment" msgstr "Archiver l'environnement" +msgid "Archive failed" +msgstr "Échec de l'archivage" + msgid "Archive workspace" msgstr "Archiver l'espace de travail" @@ -2323,6 +2330,7 @@ msgid "Archived" msgstr "Archivés" #. placeholder {0}: formatCompactRelativeTime(archivedAt, now) +#. placeholder {0}: formatCompactRelativeTime(cloud.deletedAt) msgid "Archived · {0}" msgstr "Archivé · {0}" @@ -5046,6 +5054,9 @@ msgstr "Créé par" msgid "Created by <0/> · {createdAgo}" msgstr "Créé par <0/> · {createdAgo}" +msgid "Created by me" +msgstr "Créés par moi" + msgid "Created team \"{trimmedName}\"" msgstr "Équipe « {trimmedName} » créée" @@ -5296,10 +5307,6 @@ msgstr "Supprimer" msgid "Delete \"{0}\"? This removes its worktree from the host." msgstr "Supprimer « {0} » ? Cela retire son worktree de l'hôte." -#. placeholder {0}: target.name -msgid "Delete \"{0}\"? This shuts down its sandbox and everything in it." -msgstr "Supprimer « {0} » ? Cela arrête son bac à sable et tout ce qu'il contient." - msgid "Delete \"{basename}\"?" msgstr "Supprimer « {basename} » ?" @@ -5347,9 +5354,6 @@ msgstr "Supprimer l'automatisation" msgid "Delete automation?" msgstr "Supprimer l'automatisation ?" -msgid "Delete cloud workspace" -msgstr "Supprimer l'espace de travail cloud" - #. placeholder {0}: index + 1 msgid "Delete command {0}" msgstr "Supprimer la commande {0}" @@ -9382,6 +9386,10 @@ msgstr "Mi-2027" msgid "Mine" msgstr "Les miennes" +msgctxt "workspaces I created" +msgid "Mine" +msgstr "Les miens" + msgid "Minimal reasoning effort" msgstr "Effort de raisonnement minimal" @@ -9934,6 +9942,9 @@ msgstr "Aucun agent pour l'instant." msgid "No API keys yet." msgstr "Aucune clé API pour l'instant." +msgid "No archived workspaces" +msgstr "Aucun espace de travail archivé" + msgid "No assignee" msgstr "Aucun assigné" @@ -9982,6 +9993,9 @@ msgstr "Aucun navigateur Chrome, Brave, Arc ou autre navigateur Chromium n'a ét msgid "No Claude or Codex usage found on this machine yet. Join now and you'll appear once you've used an agent." msgstr "Aucune utilisation de Claude ou Codex trouvée sur cette machine pour l'instant. Rejoignez maintenant et vous apparaîtrez dès que vous aurez utilisé un agent." +msgid "No cloud workspaces match these filters" +msgstr "Aucun espace de travail cloud ne correspond à ces filtres" + msgid "No cloud workspaces yet" msgstr "Aucun espace de travail cloud pour l'instant" @@ -10566,8 +10580,8 @@ msgstr "Rien ici." msgid "Nothing installed yet." msgstr "Rien d'installé pour le moment." -msgid "Nothing is running. Remove it and create a new one." -msgstr "Rien ne tourne. Supprimez-le et créez-en un nouveau." +msgid "Nothing is running. Archive it and create a new one." +msgstr "Rien ne tourne. Archivez-le et créez-en un nouveau." msgid "Nothing lives at {pathname}." msgstr "Il n'y a rien à l'emplacement {pathname}." @@ -12242,6 +12256,7 @@ msgid "Pull request" msgstr "Pull request" #. placeholder {0}: data.number +#. placeholder {0}: pullRequest.number #. placeholder {0}: pullRequest.prNumber msgid "Pull request #{0}" msgstr "Pull request #{0}" @@ -15000,8 +15015,8 @@ msgstr "Étape {step} : {name}" msgid "Still can't reach the host." msgstr "L'hôte reste injoignable." -msgid "Still trying. If it keeps failing, remove it and create a new one." -msgstr "Nouvelle tentative en cours. Si l'échec persiste, supprimez-le et créez-en un nouveau." +msgid "Still trying. If it keeps failing, archive it and create a new one." +msgstr "Nouvelle tentative en cours. Si l'échec persiste, archivez-le et créez-en un nouveau." msgid "Still working — large repos can take a few minutes. You can leave; the workspace will appear on Home when it's ready." msgstr "Toujours en cours — les gros dépôts peuvent prendre quelques minutes. Vous pouvez quitter ; l'espace de travail apparaîtra sur l'accueil dès qu'il sera prêt." @@ -16772,6 +16787,9 @@ msgstr "Impossible de transcrire l'audio" msgid "Unarchive" msgstr "Désarchiver" +msgid "Unarchive failed" +msgstr "Échec du désarchivage" + msgid "Unarchive workspace" msgstr "Désarchiver l'espace de travail" diff --git a/packages/i18n/locales/id/messages.po b/packages/i18n/locales/id/messages.po index e6c7e2c4f5b..4336c704f5c 100644 --- a/packages/i18n/locales/id/messages.po +++ b/packages/i18n/locales/id/messages.po @@ -1526,6 +1526,10 @@ msgstr "Aktivasi, seberapa banyak dipakai setelah masuk, dan apakah mereka kemba msgid "Active" msgstr "Aktif" +msgctxt "cloud workspace status" +msgid "Active" +msgstr "Aktif" + msgctxt "presence" msgid "Active" msgstr "Aktif" @@ -2316,6 +2320,9 @@ msgstr "Arsipkan" msgid "Archive environment" msgstr "Arsipkan environment" +msgid "Archive failed" +msgstr "Pengarsipan gagal" + msgid "Archive workspace" msgstr "Arsipkan workspace" @@ -2323,6 +2330,7 @@ msgid "Archived" msgstr "Diarsipkan" #. placeholder {0}: formatCompactRelativeTime(archivedAt, now) +#. placeholder {0}: formatCompactRelativeTime(cloud.deletedAt) msgid "Archived · {0}" msgstr "Diarsipkan · {0}" @@ -5046,6 +5054,9 @@ msgstr "Dibuat oleh" msgid "Created by <0/> · {createdAgo}" msgstr "Dibuat oleh <0/> · {createdAgo}" +msgid "Created by me" +msgstr "Dibuat oleh saya" + msgid "Created team \"{trimmedName}\"" msgstr "Membuat tim \"{trimmedName}\"" @@ -5296,10 +5307,6 @@ msgstr "Hapus" msgid "Delete \"{0}\"? This removes its worktree from the host." msgstr "Hapus \"{0}\"? Ini menghapus worktree-nya dari host." -#. placeholder {0}: target.name -msgid "Delete \"{0}\"? This shuts down its sandbox and everything in it." -msgstr "Hapus \"{0}\"? Ini mematikan sandbox-nya beserta seluruh isinya." - msgid "Delete \"{basename}\"?" msgstr "Hapus \"{basename}\"?" @@ -5347,9 +5354,6 @@ msgstr "Hapus otomatisasi" msgid "Delete automation?" msgstr "Hapus otomatisasi?" -msgid "Delete cloud workspace" -msgstr "Hapus workspace cloud" - #. placeholder {0}: index + 1 msgid "Delete command {0}" msgstr "Hapus perintah {0}" @@ -9382,6 +9386,10 @@ msgstr "Pertengahan 2027" msgid "Mine" msgstr "Milik saya" +msgctxt "workspaces I created" +msgid "Mine" +msgstr "Milik saya" + msgid "Minimal reasoning effort" msgstr "Upaya penalaran minimal" @@ -9934,6 +9942,9 @@ msgstr "Belum ada agen." msgid "No API keys yet." msgstr "Belum ada kunci API." +msgid "No archived workspaces" +msgstr "Tidak ada workspace yang diarsipkan" + msgid "No assignee" msgstr "Tanpa penerima tugas" @@ -9982,6 +9993,9 @@ msgstr "Tidak ditemukan Chrome, Brave, Arc, atau browser Chromium lainnya." msgid "No Claude or Codex usage found on this machine yet. Join now and you'll appear once you've used an agent." msgstr "Belum ada penggunaan Claude atau Codex di mesin ini. Gabung sekarang dan Anda akan muncul setelah memakai agen." +msgid "No cloud workspaces match these filters" +msgstr "Tidak ada workspace cloud yang cocok dengan filter ini" + msgid "No cloud workspaces yet" msgstr "Belum ada workspace cloud" @@ -10566,8 +10580,8 @@ msgstr "Tidak ada apa-apa di sini." msgid "Nothing installed yet." msgstr "Belum ada yang terpasang." -msgid "Nothing is running. Remove it and create a new one." -msgstr "Tidak ada yang berjalan. Hapus dan buat yang baru." +msgid "Nothing is running. Archive it and create a new one." +msgstr "Tidak ada yang berjalan. Arsipkan dan buat yang baru." msgid "Nothing lives at {pathname}." msgstr "Tidak ada apa pun di {pathname}." @@ -12242,6 +12256,7 @@ msgid "Pull request" msgstr "Pull request" #. placeholder {0}: data.number +#. placeholder {0}: pullRequest.number #. placeholder {0}: pullRequest.prNumber msgid "Pull request #{0}" msgstr "Pull request #{0}" @@ -15000,8 +15015,8 @@ msgstr "Langkah {step}: {name}" msgid "Still can't reach the host." msgstr "Host masih tidak dapat dijangkau." -msgid "Still trying. If it keeps failing, remove it and create a new one." -msgstr "Masih mencoba. Jika terus gagal, hapus dan buat yang baru." +msgid "Still trying. If it keeps failing, archive it and create a new one." +msgstr "Masih mencoba. Jika terus gagal, arsipkan dan buat yang baru." msgid "Still working — large repos can take a few minutes. You can leave; the workspace will appear on Home when it's ready." msgstr "Masih berjalan — repo besar bisa makan beberapa menit. Anda boleh keluar; workspace-nya akan muncul di Beranda saat siap." @@ -16772,6 +16787,9 @@ msgstr "Tidak bisa mentranskripsi audio" msgid "Unarchive" msgstr "Batalkan arsip" +msgid "Unarchive failed" +msgstr "Pembatalan arsip gagal" + msgid "Unarchive workspace" msgstr "Batalkan arsip workspace" diff --git a/packages/i18n/locales/it/messages.po b/packages/i18n/locales/it/messages.po index b6922a88f26..c6c30e3a098 100644 --- a/packages/i18n/locales/it/messages.po +++ b/packages/i18n/locales/it/messages.po @@ -1526,6 +1526,10 @@ msgstr "L'attivazione, quanto lo usano una volta entrati e se tornano." msgid "Active" msgstr "Attiva" +msgctxt "cloud workspace status" +msgid "Active" +msgstr "Attivi" + msgctxt "presence" msgid "Active" msgstr "Attivo" @@ -2316,6 +2320,9 @@ msgstr "Archivia" msgid "Archive environment" msgstr "Archivia ambiente" +msgid "Archive failed" +msgstr "Archiviazione non riuscita" + msgid "Archive workspace" msgstr "Archivia il workspace" @@ -2323,6 +2330,7 @@ msgid "Archived" msgstr "Archiviati" #. placeholder {0}: formatCompactRelativeTime(archivedAt, now) +#. placeholder {0}: formatCompactRelativeTime(cloud.deletedAt) msgid "Archived · {0}" msgstr "Archiviato · {0}" @@ -5046,6 +5054,9 @@ msgstr "Creato da" msgid "Created by <0/> · {createdAgo}" msgstr "Creato da <0/> · {createdAgo}" +msgid "Created by me" +msgstr "Creati da me" + msgid "Created team \"{trimmedName}\"" msgstr "Team \"{trimmedName}\" creato" @@ -5296,10 +5307,6 @@ msgstr "Elimina" msgid "Delete \"{0}\"? This removes its worktree from the host." msgstr "Eliminare \"{0}\"? Questo rimuove il suo worktree dall'host." -#. placeholder {0}: target.name -msgid "Delete \"{0}\"? This shuts down its sandbox and everything in it." -msgstr "Eliminare \"{0}\"? Questo spegne la sua sandbox e tutto ciò che contiene." - msgid "Delete \"{basename}\"?" msgstr "Eliminare \"{basename}\"?" @@ -5347,9 +5354,6 @@ msgstr "Elimina l'automazione" msgid "Delete automation?" msgstr "Eliminare l'automazione?" -msgid "Delete cloud workspace" -msgstr "Elimina il workspace cloud" - #. placeholder {0}: index + 1 msgid "Delete command {0}" msgstr "Elimina il comando {0}" @@ -9382,6 +9386,10 @@ msgstr "Metà 2027" msgid "Mine" msgstr "Mie" +msgctxt "workspaces I created" +msgid "Mine" +msgstr "Miei" + msgid "Minimal reasoning effort" msgstr "Ragionamento minimo" @@ -9934,6 +9942,9 @@ msgstr "Ancora nessun agente." msgid "No API keys yet." msgstr "Ancora nessuna chiave API." +msgid "No archived workspaces" +msgstr "Nessun workspace archiviato" + msgid "No assignee" msgstr "Nessun assegnatario" @@ -9982,6 +9993,9 @@ msgstr "Non sono stati trovati Chrome, Brave, Arc o altri browser Chromium." msgid "No Claude or Codex usage found on this machine yet. Join now and you'll appear once you've used an agent." msgstr "Nessun utilizzo di Claude o Codex trovato su questa macchina. Unisciti ora e comparirai dopo aver usato un agente." +msgid "No cloud workspaces match these filters" +msgstr "Nessun workspace cloud corrisponde a questi filtri" + msgid "No cloud workspaces yet" msgstr "Ancora nessun workspace cloud" @@ -10566,8 +10580,8 @@ msgstr "Niente qui." msgid "Nothing installed yet." msgstr "Ancora nulla di installato." -msgid "Nothing is running. Remove it and create a new one." -msgstr "Non è in esecuzione nulla. Rimuovilo e creane uno nuovo." +msgid "Nothing is running. Archive it and create a new one." +msgstr "Non è in esecuzione nulla. Archivialo e creane uno nuovo." msgid "Nothing lives at {pathname}." msgstr "Non c'è niente in {pathname}." @@ -12242,6 +12256,7 @@ msgid "Pull request" msgstr "Pull request" #. placeholder {0}: data.number +#. placeholder {0}: pullRequest.number #. placeholder {0}: pullRequest.prNumber msgid "Pull request #{0}" msgstr "Pull request #{0}" @@ -15000,8 +15015,8 @@ msgstr "Passaggio {step}: {name}" msgid "Still can't reach the host." msgstr "L'host resta irraggiungibile." -msgid "Still trying. If it keeps failing, remove it and create a new one." -msgstr "Nuovo tentativo in corso. Se il problema persiste, rimuovilo e creane uno nuovo." +msgid "Still trying. If it keeps failing, archive it and create a new one." +msgstr "Nuovo tentativo in corso. Se il problema persiste, archivialo e creane uno nuovo." msgid "Still working — large repos can take a few minutes. You can leave; the workspace will appear on Home when it's ready." msgstr "Ancora al lavoro — i repo grandi possono richiedere qualche minuto. Puoi uscire; il workspace comparirà nella Home quando è pronto." @@ -16772,6 +16787,9 @@ msgstr "Impossibile trascrivere l'audio" msgid "Unarchive" msgstr "Ripristina dall'archivio" +msgid "Unarchive failed" +msgstr "Ripristino dall'archivio non riuscito" + msgid "Unarchive workspace" msgstr "Ripristina il workspace dall'archivio" diff --git a/packages/i18n/locales/ja/messages.po b/packages/i18n/locales/ja/messages.po index b38e4ed6183..907a946e4b6 100644 --- a/packages/i18n/locales/ja/messages.po +++ b/packages/i18n/locales/ja/messages.po @@ -1526,6 +1526,10 @@ msgstr "アクティベーション、使い始めてからの利用量、そし msgid "Active" msgstr "有効" +msgctxt "cloud workspace status" +msgid "Active" +msgstr "アクティブ" + msgctxt "presence" msgid "Active" msgstr "アクティブ" @@ -2316,6 +2320,9 @@ msgstr "アーカイブ" msgid "Archive environment" msgstr "環境をアーカイブ" +msgid "Archive failed" +msgstr "アーカイブに失敗しました" + msgid "Archive workspace" msgstr "ワークスペースをアーカイブ" @@ -2323,6 +2330,7 @@ msgid "Archived" msgstr "アーカイブ済み" #. placeholder {0}: formatCompactRelativeTime(archivedAt, now) +#. placeholder {0}: formatCompactRelativeTime(cloud.deletedAt) msgid "Archived · {0}" msgstr "アーカイブ済み · {0}" @@ -5046,6 +5054,9 @@ msgstr "作成者" msgid "Created by <0/> · {createdAgo}" msgstr "作成者 <0/> · {createdAgo}" +msgid "Created by me" +msgstr "自分が作成" + msgid "Created team \"{trimmedName}\"" msgstr "チーム「{trimmedName}」を作成しました" @@ -5296,10 +5307,6 @@ msgstr "削除" msgid "Delete \"{0}\"? This removes its worktree from the host." msgstr "「{0}」を削除しますか?ホストからワークツリーが削除されます。" -#. placeholder {0}: target.name -msgid "Delete \"{0}\"? This shuts down its sandbox and everything in it." -msgstr "「{0}」を削除しますか?サンドボックスとその中身がすべて停止されます。" - msgid "Delete \"{basename}\"?" msgstr "「{basename}」を削除しますか?" @@ -5347,9 +5354,6 @@ msgstr "オートメーションを削除" msgid "Delete automation?" msgstr "オートメーションを削除しますか?" -msgid "Delete cloud workspace" -msgstr "クラウドワークスペースを削除" - #. placeholder {0}: index + 1 msgid "Delete command {0}" msgstr "コマンド{0}を削除" @@ -9382,6 +9386,10 @@ msgstr "2027年半ば" msgid "Mine" msgstr "自分" +msgctxt "workspaces I created" +msgid "Mine" +msgstr "自分" + msgid "Minimal reasoning effort" msgstr "最小限の推論" @@ -9934,6 +9942,9 @@ msgstr "エージェントはまだありません。" msgid "No API keys yet." msgstr "APIキーはまだありません。" +msgid "No archived workspaces" +msgstr "アーカイブ済みのワークスペースはありません" + msgid "No assignee" msgstr "担当者なし" @@ -9982,6 +9993,9 @@ msgstr "Chrome、Brave、ArcなどのChromiumブラウザが見つかりませ msgid "No Claude or Codex usage found on this machine yet. Join now and you'll appear once you've used an agent." msgstr "このマシンでClaudeやCodexの使用がまだ見つかりません。今参加すれば、エージェントを使い始めた時点で表示されます。" +msgid "No cloud workspaces match these filters" +msgstr "これらのフィルターに一致するクラウドワークスペースはありません" + msgid "No cloud workspaces yet" msgstr "クラウドワークスペースはまだありません" @@ -10566,8 +10580,8 @@ msgstr "何もありません。" msgid "Nothing installed yet." msgstr "まだ何もインストールされていません。" -msgid "Nothing is running. Remove it and create a new one." -msgstr "何も実行されていません。削除して新しく作成してください。" +msgid "Nothing is running. Archive it and create a new one." +msgstr "何も実行されていません。アーカイブして新しく作成してください。" msgid "Nothing lives at {pathname}." msgstr "{pathname}には何もありません。" @@ -12242,6 +12256,7 @@ msgid "Pull request" msgstr "プルリクエスト" #. placeholder {0}: data.number +#. placeholder {0}: pullRequest.number #. placeholder {0}: pullRequest.prNumber msgid "Pull request #{0}" msgstr "プルリクエスト #{0}" @@ -15000,8 +15015,8 @@ msgstr "ステップ{step}: {name}" msgid "Still can't reach the host." msgstr "ホストにまだ接続できません。" -msgid "Still trying. If it keeps failing, remove it and create a new one." -msgstr "再試行しています。失敗が続く場合は、削除して新しく作成してください。" +msgid "Still trying. If it keeps failing, archive it and create a new one." +msgstr "再試行しています。失敗が続く場合は、アーカイブして新しく作成してください。" msgid "Still working — large repos can take a few minutes. You can leave; the workspace will appear on Home when it's ready." msgstr "処理を続けています — 大きなリポジトリでは数分かかることがあります。画面を離れても構いません。準備ができるとホームに表示されます。" @@ -16772,6 +16787,9 @@ msgstr "音声を文字起こしできません" msgid "Unarchive" msgstr "アーカイブ解除" +msgid "Unarchive failed" +msgstr "アーカイブ解除に失敗しました" + msgid "Unarchive workspace" msgstr "ワークスペースのアーカイブを解除" diff --git a/packages/i18n/locales/ko/messages.po b/packages/i18n/locales/ko/messages.po index 7ce5d993abb..6c74b52fcd5 100644 --- a/packages/i18n/locales/ko/messages.po +++ b/packages/i18n/locales/ko/messages.po @@ -1526,6 +1526,10 @@ msgstr "활성화, 들어온 뒤 얼마나 쓰는지, 그리고 다시 돌아오 msgid "Active" msgstr "활성" +msgctxt "cloud workspace status" +msgid "Active" +msgstr "활성" + msgctxt "presence" msgid "Active" msgstr "활성" @@ -2316,6 +2320,9 @@ msgstr "보관" msgid "Archive environment" msgstr "환경 보관" +msgid "Archive failed" +msgstr "보관 실패" + msgid "Archive workspace" msgstr "워크스페이스 보관" @@ -2323,6 +2330,7 @@ msgid "Archived" msgstr "보관됨" #. placeholder {0}: formatCompactRelativeTime(archivedAt, now) +#. placeholder {0}: formatCompactRelativeTime(cloud.deletedAt) msgid "Archived · {0}" msgstr "보관됨 · {0}" @@ -5046,6 +5054,9 @@ msgstr "만든 사람" msgid "Created by <0/> · {createdAgo}" msgstr "만든 사람 <0/> · {createdAgo}" +msgid "Created by me" +msgstr "내가 만든 항목" + msgid "Created team \"{trimmedName}\"" msgstr "\"{trimmedName}\" 팀을 만들었습니다" @@ -5296,10 +5307,6 @@ msgstr "삭제" msgid "Delete \"{0}\"? This removes its worktree from the host." msgstr "\"{0}\"을(를) 삭제할까요? 호스트에서 해당 worktree가 제거됩니다." -#. placeholder {0}: target.name -msgid "Delete \"{0}\"? This shuts down its sandbox and everything in it." -msgstr "\"{0}\"을(를) 삭제할까요? 샌드박스와 그 안의 모든 것이 종료됩니다." - msgid "Delete \"{basename}\"?" msgstr "\"{basename}\"을(를) 삭제할까요?" @@ -5347,9 +5354,6 @@ msgstr "자동화 삭제" msgid "Delete automation?" msgstr "자동화를 삭제할까요?" -msgid "Delete cloud workspace" -msgstr "클라우드 워크스페이스 삭제" - #. placeholder {0}: index + 1 msgid "Delete command {0}" msgstr "명령 {0} 삭제" @@ -9382,6 +9386,10 @@ msgstr "2027년 중반" msgid "Mine" msgstr "내 자동화" +msgctxt "workspaces I created" +msgid "Mine" +msgstr "내 워크스페이스" + msgid "Minimal reasoning effort" msgstr "최소한의 추론" @@ -9934,6 +9942,9 @@ msgstr "아직 에이전트가 없습니다." msgid "No API keys yet." msgstr "아직 API 키가 없습니다." +msgid "No archived workspaces" +msgstr "보관된 워크스페이스가 없습니다" + msgid "No assignee" msgstr "담당자 없음" @@ -9982,6 +9993,9 @@ msgstr "Chrome, Brave, Arc 등 Chromium 기반 브라우저를 찾을 수 없습 msgid "No Claude or Codex usage found on this machine yet. Join now and you'll appear once you've used an agent." msgstr "이 컴퓨터에서 아직 Claude 또는 Codex 사용량을 찾지 못했습니다. 지금 참여하면 에이전트를 사용한 뒤 순위에 표시됩니다." +msgid "No cloud workspaces match these filters" +msgstr "이 필터와 일치하는 클라우드 워크스페이스가 없습니다" + msgid "No cloud workspaces yet" msgstr "아직 클라우드 워크스페이스가 없습니다" @@ -10566,8 +10580,8 @@ msgstr "아무것도 없습니다." msgid "Nothing installed yet." msgstr "아직 설치된 항목이 없습니다." -msgid "Nothing is running. Remove it and create a new one." -msgstr "실행 중인 것이 없습니다. 제거하고 새로 만드세요." +msgid "Nothing is running. Archive it and create a new one." +msgstr "실행 중인 것이 없습니다. 보관하고 새로 만드세요." msgid "Nothing lives at {pathname}." msgstr "{pathname}에는 아무것도 없습니다." @@ -12242,6 +12256,7 @@ msgid "Pull request" msgstr "풀 리퀘스트" #. placeholder {0}: data.number +#. placeholder {0}: pullRequest.number #. placeholder {0}: pullRequest.prNumber msgid "Pull request #{0}" msgstr "풀 리퀘스트 #{0}" @@ -15000,8 +15015,8 @@ msgstr "{step}단계: {name}" msgid "Still can't reach the host." msgstr "아직 호스트에 연결할 수 없습니다." -msgid "Still trying. If it keeps failing, remove it and create a new one." -msgstr "계속 시도하는 중입니다. 계속 실패하면 제거하고 새로 만드세요." +msgid "Still trying. If it keeps failing, archive it and create a new one." +msgstr "계속 시도하는 중입니다. 계속 실패하면 보관하고 새로 만드세요." msgid "Still working — large repos can take a few minutes. You can leave; the workspace will appear on Home when it's ready." msgstr "아직 진행 중입니다 — 큰 리포지토리는 몇 분 걸릴 수 있습니다. 나가도 되며, 준비되면 홈에 워크스페이스가 표시됩니다." @@ -16772,6 +16787,9 @@ msgstr "오디오를 변환할 수 없습니다" msgid "Unarchive" msgstr "보관 해제" +msgid "Unarchive failed" +msgstr "보관 해제 실패" + msgid "Unarchive workspace" msgstr "워크스페이스 보관 해제" diff --git a/packages/i18n/locales/nl/messages.po b/packages/i18n/locales/nl/messages.po index 8dbb6cf7eb6..ffb7b4d7e8a 100644 --- a/packages/i18n/locales/nl/messages.po +++ b/packages/i18n/locales/nl/messages.po @@ -1526,6 +1526,10 @@ msgstr "Activatie, hoeveel mensen het gebruiken zodra ze binnen zijn, en of ze t msgid "Active" msgstr "Actief" +msgctxt "cloud workspace status" +msgid "Active" +msgstr "Actief" + msgctxt "presence" msgid "Active" msgstr "Actief" @@ -2316,6 +2320,9 @@ msgstr "Archiveren" msgid "Archive environment" msgstr "Omgeving archiveren" +msgid "Archive failed" +msgstr "Archiveren mislukt" + msgid "Archive workspace" msgstr "Werkruimte archiveren" @@ -2323,6 +2330,7 @@ msgid "Archived" msgstr "Gearchiveerd" #. placeholder {0}: formatCompactRelativeTime(archivedAt, now) +#. placeholder {0}: formatCompactRelativeTime(cloud.deletedAt) msgid "Archived · {0}" msgstr "Gearchiveerd · {0}" @@ -5046,6 +5054,9 @@ msgstr "Gemaakt door" msgid "Created by <0/> · {createdAgo}" msgstr "Aangemaakt door <0/> · {createdAgo}" +msgid "Created by me" +msgstr "Door mij gemaakt" + msgid "Created team \"{trimmedName}\"" msgstr "Team \"{trimmedName}\" aangemaakt" @@ -5296,10 +5307,6 @@ msgstr "Verwijderen" msgid "Delete \"{0}\"? This removes its worktree from the host." msgstr "\"{0}\" verwijderen? Hiermee wordt de worktree van de host verwijderd." -#. placeholder {0}: target.name -msgid "Delete \"{0}\"? This shuts down its sandbox and everything in it." -msgstr "\"{0}\" verwijderen? Hiermee wordt de sandbox met alles erin afgesloten." - msgid "Delete \"{basename}\"?" msgstr "\"{basename}\" verwijderen?" @@ -5347,9 +5354,6 @@ msgstr "Automatisering verwijderen" msgid "Delete automation?" msgstr "Automatisering verwijderen?" -msgid "Delete cloud workspace" -msgstr "Cloudwerkruimte verwijderen" - #. placeholder {0}: index + 1 msgid "Delete command {0}" msgstr "Commando {0} verwijderen" @@ -9382,6 +9386,10 @@ msgstr "Medio 2027" msgid "Mine" msgstr "Mijn" +msgctxt "workspaces I created" +msgid "Mine" +msgstr "Mijn" + msgid "Minimal reasoning effort" msgstr "Minimale redeneerinspanning" @@ -9934,6 +9942,9 @@ msgstr "Nog geen agents." msgid "No API keys yet." msgstr "Nog geen API-sleutels." +msgid "No archived workspaces" +msgstr "Geen gearchiveerde werkruimtes" + msgid "No assignee" msgstr "Geen toegewezen persoon" @@ -9982,6 +9993,9 @@ msgstr "Er zijn geen Chrome-, Brave-, Arc- of andere Chromium-browsers gevonden. msgid "No Claude or Codex usage found on this machine yet. Join now and you'll appear once you've used an agent." msgstr "Nog geen Claude- of Codex-gebruik gevonden op deze machine. Doe nu mee, dan verschijn je zodra je een agent hebt gebruikt." +msgid "No cloud workspaces match these filters" +msgstr "Geen cloudwerkruimtes komen overeen met deze filters" + msgid "No cloud workspaces yet" msgstr "Nog geen cloudwerkruimtes" @@ -10566,8 +10580,8 @@ msgstr "Hier is niets." msgid "Nothing installed yet." msgstr "Nog niets geïnstalleerd." -msgid "Nothing is running. Remove it and create a new one." -msgstr "Er draait niets. Verwijder deze en maak een nieuwe aan." +msgid "Nothing is running. Archive it and create a new one." +msgstr "Er draait niets. Archiveer deze en maak een nieuwe aan." msgid "Nothing lives at {pathname}." msgstr "Er staat niets op {pathname}." @@ -12242,6 +12256,7 @@ msgid "Pull request" msgstr "Pull request" #. placeholder {0}: data.number +#. placeholder {0}: pullRequest.number #. placeholder {0}: pullRequest.prNumber msgid "Pull request #{0}" msgstr "Pull request #{0}" @@ -15000,8 +15015,8 @@ msgstr "Stap {step}: {name}" msgid "Still can't reach the host." msgstr "De host is nog steeds onbereikbaar." -msgid "Still trying. If it keeps failing, remove it and create a new one." -msgstr "We blijven het proberen. Als het blijft mislukken, verwijder deze en maak een nieuwe aan." +msgid "Still trying. If it keeps failing, archive it and create a new one." +msgstr "We blijven het proberen. Als het blijft mislukken, archiveer deze en maak een nieuwe aan." msgid "Still working — large repos can take a few minutes. You can leave; the workspace will appear on Home when it's ready." msgstr "Nog bezig — grote repo's kunnen een paar minuten duren. Je kunt weggaan; de werkruimte verschijnt op Home zodra die klaar is." @@ -16772,6 +16787,9 @@ msgstr "Kan audio niet transcriberen" msgid "Unarchive" msgstr "Dearchiveren" +msgid "Unarchive failed" +msgstr "Dearchiveren mislukt" + msgid "Unarchive workspace" msgstr "Werkruimte dearchiveren" diff --git a/packages/i18n/locales/pl/messages.po b/packages/i18n/locales/pl/messages.po index ecef3e80851..95833af5ee8 100644 --- a/packages/i18n/locales/pl/messages.po +++ b/packages/i18n/locales/pl/messages.po @@ -1526,6 +1526,10 @@ msgstr "Aktywacja, ile korzystają po wejściu i czy wracają." msgid "Active" msgstr "Aktywna" +msgctxt "cloud workspace status" +msgid "Active" +msgstr "Aktywne" + msgctxt "presence" msgid "Active" msgstr "Aktywny" @@ -2316,6 +2320,9 @@ msgstr "Archiwizuj" msgid "Archive environment" msgstr "Archiwizuj środowisko" +msgid "Archive failed" +msgstr "Archiwizacja nie powiodła się" + msgid "Archive workspace" msgstr "Archiwizuj obszar roboczy" @@ -2323,6 +2330,7 @@ msgid "Archived" msgstr "Zarchiwizowane" #. placeholder {0}: formatCompactRelativeTime(archivedAt, now) +#. placeholder {0}: formatCompactRelativeTime(cloud.deletedAt) msgid "Archived · {0}" msgstr "Zarchiwizowano · {0}" @@ -5046,6 +5054,9 @@ msgstr "Utworzone przez" msgid "Created by <0/> · {createdAgo}" msgstr "Utworzone przez <0/> · {createdAgo}" +msgid "Created by me" +msgstr "Utworzone przeze mnie" + msgid "Created team \"{trimmedName}\"" msgstr "Utworzono zespół „{trimmedName}”" @@ -5296,10 +5307,6 @@ msgstr "Usuń" msgid "Delete \"{0}\"? This removes its worktree from the host." msgstr "Usunąć „{0}”? Usunie to jego worktree z hosta." -#. placeholder {0}: target.name -msgid "Delete \"{0}\"? This shuts down its sandbox and everything in it." -msgstr "Usunąć „{0}”? Wyłączy to jego piaskownicę i wszystko w niej." - msgid "Delete \"{basename}\"?" msgstr "Usunąć „{basename}”?" @@ -5347,9 +5354,6 @@ msgstr "Usuń automatyzację" msgid "Delete automation?" msgstr "Usunąć automatyzację?" -msgid "Delete cloud workspace" -msgstr "Usuń obszar roboczy w chmurze" - #. placeholder {0}: index + 1 msgid "Delete command {0}" msgstr "Usuń polecenie {0}" @@ -9382,6 +9386,10 @@ msgstr "Połowa 2027" msgid "Mine" msgstr "Moje" +msgctxt "workspaces I created" +msgid "Mine" +msgstr "Moje" + msgid "Minimal reasoning effort" msgstr "Minimalne rozumowanie" @@ -9934,6 +9942,9 @@ msgstr "Brak agentów." msgid "No API keys yet." msgstr "Brak kluczy API." +msgid "No archived workspaces" +msgstr "Brak zarchiwizowanych obszarów roboczych" + msgid "No assignee" msgstr "Brak przypisania" @@ -9982,6 +9993,9 @@ msgstr "Nie znaleziono przeglądarek Chrome, Brave, Arc ani innych opartych na C msgid "No Claude or Codex usage found on this machine yet. Join now and you'll appear once you've used an agent." msgstr "Nie znaleziono jeszcze zużycia Claude ani Codex na tej maszynie. Dołącz teraz, a pojawisz się po pierwszym użyciu agenta." +msgid "No cloud workspaces match these filters" +msgstr "Żadne obszary robocze w chmurze nie pasują do tych filtrów" + msgid "No cloud workspaces yet" msgstr "Brak obszarów roboczych w chmurze" @@ -10566,8 +10580,8 @@ msgstr "Nic tu nie ma." msgid "Nothing installed yet." msgstr "Nic jeszcze nie zainstalowano." -msgid "Nothing is running. Remove it and create a new one." -msgstr "Nic nie działa. Usuń go i utwórz nowy." +msgid "Nothing is running. Archive it and create a new one." +msgstr "Nic nie działa. Zarchiwizuj go i utwórz nowy." msgid "Nothing lives at {pathname}." msgstr "Pod {pathname} nic nie ma." @@ -12242,6 +12256,7 @@ msgid "Pull request" msgstr "Pull request" #. placeholder {0}: data.number +#. placeholder {0}: pullRequest.number #. placeholder {0}: pullRequest.prNumber msgid "Pull request #{0}" msgstr "Pull request #{0}" @@ -15000,8 +15015,8 @@ msgstr "Krok {step}: {name}" msgid "Still can't reach the host." msgstr "Host nadal jest nieosiągalny." -msgid "Still trying. If it keeps failing, remove it and create a new one." -msgstr "Wciąż próbujemy. Jeśli nadal się nie uda, usuń go i utwórz nowy." +msgid "Still trying. If it keeps failing, archive it and create a new one." +msgstr "Wciąż próbujemy. Jeśli nadal się nie uda, zarchiwizuj go i utwórz nowy." msgid "Still working — large repos can take a few minutes. You can leave; the workspace will appear on Home when it's ready." msgstr "Wciąż trwa — duże repozytoria mogą zająć kilka minut. Możesz wyjść; obszar roboczy pojawi się na ekranie głównym, gdy będzie gotowy." @@ -16772,6 +16787,9 @@ msgstr "Nie można przetranskrybować dźwięku" msgid "Unarchive" msgstr "Przywróć z archiwum" +msgid "Unarchive failed" +msgstr "Przywracanie z archiwum nie powiodło się" + msgid "Unarchive workspace" msgstr "Przywróć obszar roboczy z archiwum" diff --git a/packages/i18n/locales/pt-BR/messages.po b/packages/i18n/locales/pt-BR/messages.po index 77ad8908c17..98a0be94614 100644 --- a/packages/i18n/locales/pt-BR/messages.po +++ b/packages/i18n/locales/pt-BR/messages.po @@ -1526,6 +1526,10 @@ msgstr "Ativação, quanto as pessoas usam depois de entrar e se voltam." msgid "Active" msgstr "Ativa" +msgctxt "cloud workspace status" +msgid "Active" +msgstr "Ativos" + msgctxt "presence" msgid "Active" msgstr "Ativo" @@ -2316,6 +2320,9 @@ msgstr "Arquivar" msgid "Archive environment" msgstr "Arquivar ambiente" +msgid "Archive failed" +msgstr "Falha ao arquivar" + msgid "Archive workspace" msgstr "Arquivar área de trabalho" @@ -2323,6 +2330,7 @@ msgid "Archived" msgstr "Arquivadas" #. placeholder {0}: formatCompactRelativeTime(archivedAt, now) +#. placeholder {0}: formatCompactRelativeTime(cloud.deletedAt) msgid "Archived · {0}" msgstr "Arquivado · {0}" @@ -5046,6 +5054,9 @@ msgstr "Criado por" msgid "Created by <0/> · {createdAgo}" msgstr "Criado por <0/> · {createdAgo}" +msgid "Created by me" +msgstr "Criados por mim" + msgid "Created team \"{trimmedName}\"" msgstr "Equipe \"{trimmedName}\" criada" @@ -5296,10 +5307,6 @@ msgstr "Excluir" msgid "Delete \"{0}\"? This removes its worktree from the host." msgstr "Excluir \"{0}\"? Isso remove a worktree dela do host." -#. placeholder {0}: target.name -msgid "Delete \"{0}\"? This shuts down its sandbox and everything in it." -msgstr "Excluir \"{0}\"? Isso encerra o sandbox dela e tudo que há nele." - msgid "Delete \"{basename}\"?" msgstr "Excluir \"{basename}\"?" @@ -5347,9 +5354,6 @@ msgstr "Excluir automação" msgid "Delete automation?" msgstr "Excluir a automação?" -msgid "Delete cloud workspace" -msgstr "Excluir área de trabalho na nuvem" - #. placeholder {0}: index + 1 msgid "Delete command {0}" msgstr "Excluir o comando {0}" @@ -9382,6 +9386,10 @@ msgstr "Meados de 2027" msgid "Mine" msgstr "Minhas" +msgctxt "workspaces I created" +msgid "Mine" +msgstr "Meus" + msgid "Minimal reasoning effort" msgstr "Esforço de raciocínio mínimo" @@ -9934,6 +9942,9 @@ msgstr "Nenhum agente ainda." msgid "No API keys yet." msgstr "Nenhuma chave de API ainda." +msgid "No archived workspaces" +msgstr "Nenhuma área de trabalho arquivada" + msgid "No assignee" msgstr "Sem responsável" @@ -9982,6 +9993,9 @@ msgstr "Nenhum navegador Chrome, Brave, Arc ou outro baseado em Chromium foi enc msgid "No Claude or Codex usage found on this machine yet. Join now and you'll appear once you've used an agent." msgstr "Nenhum uso do Claude ou do Codex encontrado nesta máquina ainda. Entre agora e você aparecerá assim que usar um agente." +msgid "No cloud workspaces match these filters" +msgstr "Nenhuma área de trabalho na nuvem corresponde a esses filtros" + msgid "No cloud workspaces yet" msgstr "Nenhuma área de trabalho na nuvem ainda" @@ -10566,8 +10580,8 @@ msgstr "Nada aqui." msgid "Nothing installed yet." msgstr "Nada instalado ainda." -msgid "Nothing is running. Remove it and create a new one." -msgstr "Nada está rodando. Remova e crie outra." +msgid "Nothing is running. Archive it and create a new one." +msgstr "Nada está rodando. Arquive e crie outra." msgid "Nothing lives at {pathname}." msgstr "Não há nada em {pathname}." @@ -12242,6 +12256,7 @@ msgid "Pull request" msgstr "Pull request" #. placeholder {0}: data.number +#. placeholder {0}: pullRequest.number #. placeholder {0}: pullRequest.prNumber msgid "Pull request #{0}" msgstr "Pull request #{0}" @@ -15000,8 +15015,8 @@ msgstr "Etapa {step}: {name}" msgid "Still can't reach the host." msgstr "O host continua inacessível." -msgid "Still trying. If it keeps failing, remove it and create a new one." -msgstr "Ainda tentando. Se continuar falhando, remova e crie outra." +msgid "Still trying. If it keeps failing, archive it and create a new one." +msgstr "Ainda tentando. Se continuar falhando, arquive e crie outra." msgid "Still working — large repos can take a few minutes. You can leave; the workspace will appear on Home when it's ready." msgstr "Ainda trabalhando — repositórios grandes podem levar alguns minutos. Você pode sair; a área de trabalho aparece no Início quando ficar pronta." @@ -16772,6 +16787,9 @@ msgstr "Não foi possível transcrever o áudio" msgid "Unarchive" msgstr "Desarquivar" +msgid "Unarchive failed" +msgstr "Falha ao desarquivar" + msgid "Unarchive workspace" msgstr "Desarquivar área de trabalho" diff --git a/packages/i18n/locales/ru/messages.po b/packages/i18n/locales/ru/messages.po index 4c8beb639f9..604cfe4711a 100644 --- a/packages/i18n/locales/ru/messages.po +++ b/packages/i18n/locales/ru/messages.po @@ -1526,6 +1526,10 @@ msgstr "Активация, насколько активно им пользу msgid "Active" msgstr "Активна" +msgctxt "cloud workspace status" +msgid "Active" +msgstr "Активные" + msgctxt "presence" msgid "Active" msgstr "В сети" @@ -2316,6 +2320,9 @@ msgstr "Архивировать" msgid "Archive environment" msgstr "Архивировать окружение" +msgid "Archive failed" +msgstr "Не удалось архивировать" + msgid "Archive workspace" msgstr "Архивировать рабочую область" @@ -2323,6 +2330,7 @@ msgid "Archived" msgstr "Архив" #. placeholder {0}: formatCompactRelativeTime(archivedAt, now) +#. placeholder {0}: formatCompactRelativeTime(cloud.deletedAt) msgid "Archived · {0}" msgstr "В архиве · {0}" @@ -5046,6 +5054,9 @@ msgstr "Кем создано" msgid "Created by <0/> · {createdAgo}" msgstr "Создатель: <0/> · {createdAgo}" +msgid "Created by me" +msgstr "Созданные мной" + msgid "Created team \"{trimmedName}\"" msgstr "Команда «{trimmedName}» создана" @@ -5296,10 +5307,6 @@ msgstr "Удалить" msgid "Delete \"{0}\"? This removes its worktree from the host." msgstr "Удалить «{0}»? Это удалит её worktree с хоста." -#. placeholder {0}: target.name -msgid "Delete \"{0}\"? This shuts down its sandbox and everything in it." -msgstr "Удалить «{0}»? Это выключит её песочницу и всё, что в ней." - msgid "Delete \"{basename}\"?" msgstr "Удалить «{basename}»?" @@ -5347,9 +5354,6 @@ msgstr "Удалить автоматизацию" msgid "Delete automation?" msgstr "Удалить автоматизацию?" -msgid "Delete cloud workspace" -msgstr "Удалить облачную рабочую область" - #. placeholder {0}: index + 1 msgid "Delete command {0}" msgstr "Удалить команду {0}" @@ -9382,6 +9386,10 @@ msgstr "Середина 2027" msgid "Mine" msgstr "Мои" +msgctxt "workspaces I created" +msgid "Mine" +msgstr "Мои" + msgid "Minimal reasoning effort" msgstr "Минимальные рассуждения" @@ -9934,6 +9942,9 @@ msgstr "Агентов пока нет." msgid "No API keys yet." msgstr "Ключей API пока нет." +msgid "No archived workspaces" +msgstr "Нет архивированных рабочих областей" + msgid "No assignee" msgstr "Без исполнителя" @@ -9982,6 +9993,9 @@ msgstr "Chrome, Brave, Arc и другие браузеры на Chromium не msgid "No Claude or Codex usage found on this machine yet. Join now and you'll appear once you've used an agent." msgstr "На этой машине пока нет данных об использовании Claude или Codex. Присоединяйтесь — вы появитесь, как только поработаете с агентом." +msgid "No cloud workspaces match these filters" +msgstr "Нет облачных рабочих областей, подходящих под эти фильтры" + msgid "No cloud workspaces yet" msgstr "Облачных рабочих областей пока нет" @@ -10566,8 +10580,8 @@ msgstr "Здесь пусто." msgid "Nothing installed yet." msgstr "Пока ничего не установлено." -msgid "Nothing is running. Remove it and create a new one." -msgstr "Ничего не запущено. Удалите её и создайте новую." +msgid "Nothing is running. Archive it and create a new one." +msgstr "Ничего не запущено. Архивируйте её и создайте новую." msgid "Nothing lives at {pathname}." msgstr "По адресу {pathname} ничего нет." @@ -12242,6 +12256,7 @@ msgid "Pull request" msgstr "Пул-реквест" #. placeholder {0}: data.number +#. placeholder {0}: pullRequest.number #. placeholder {0}: pullRequest.prNumber msgid "Pull request #{0}" msgstr "Пул-реквест #{0}" @@ -15000,8 +15015,8 @@ msgstr "Шаг {step}: {name}" msgid "Still can't reach the host." msgstr "Хост по-прежнему недоступен." -msgid "Still trying. If it keeps failing, remove it and create a new one." -msgstr "Продолжаем попытки. Если не получится, удалите её и создайте новую." +msgid "Still trying. If it keeps failing, archive it and create a new one." +msgstr "Продолжаем попытки. Если не получится, архивируйте её и создайте новую." msgid "Still working — large repos can take a few minutes. You can leave; the workspace will appear on Home when it's ready." msgstr "Ещё работаем — большие репозитории могут занять несколько минут. Можно выйти; рабочая область появится на главной, когда будет готова." @@ -16772,6 +16787,9 @@ msgstr "Не удалось расшифровать аудио" msgid "Unarchive" msgstr "Вернуть из архива" +msgid "Unarchive failed" +msgstr "Не удалось вернуть из архива" + msgid "Unarchive workspace" msgstr "Вернуть рабочую область из архива" diff --git a/packages/i18n/locales/tr/messages.po b/packages/i18n/locales/tr/messages.po index 4ae17048212..35f1136aca1 100644 --- a/packages/i18n/locales/tr/messages.po +++ b/packages/i18n/locales/tr/messages.po @@ -1526,6 +1526,10 @@ msgstr "Aktivasyon, içeri girdikten sonra ne kadar kullandıkları ve geri dön msgid "Active" msgstr "Etkin" +msgctxt "cloud workspace status" +msgid "Active" +msgstr "Etkin" + msgctxt "presence" msgid "Active" msgstr "Etkin" @@ -2316,6 +2320,9 @@ msgstr "Arşivle" msgid "Archive environment" msgstr "Ortamı arşivle" +msgid "Archive failed" +msgstr "Arşivlenemedi" + msgid "Archive workspace" msgstr "Çalışma alanını arşivle" @@ -2323,6 +2330,7 @@ msgid "Archived" msgstr "Arşivlenmiş" #. placeholder {0}: formatCompactRelativeTime(archivedAt, now) +#. placeholder {0}: formatCompactRelativeTime(cloud.deletedAt) msgid "Archived · {0}" msgstr "Arşivlendi · {0}" @@ -5046,6 +5054,9 @@ msgstr "Oluşturan" msgid "Created by <0/> · {createdAgo}" msgstr "Oluşturan: <0/> · {createdAgo}" +msgid "Created by me" +msgstr "Benim oluşturduklarım" + msgid "Created team \"{trimmedName}\"" msgstr "\"{trimmedName}\" takımı oluşturuldu" @@ -5296,10 +5307,6 @@ msgstr "Sil" msgid "Delete \"{0}\"? This removes its worktree from the host." msgstr "\"{0}\" silinsin mi? Bu, worktree'sini host'tan kaldırır." -#. placeholder {0}: target.name -msgid "Delete \"{0}\"? This shuts down its sandbox and everything in it." -msgstr "\"{0}\" silinsin mi? Bu, sandbox'ını ve içindeki her şeyi kapatır." - msgid "Delete \"{basename}\"?" msgstr "\"{basename}\" silinsin mi?" @@ -5347,9 +5354,6 @@ msgstr "Otomasyonu sil" msgid "Delete automation?" msgstr "Otomasyon silinsin mi?" -msgid "Delete cloud workspace" -msgstr "Bulut çalışma alanını sil" - #. placeholder {0}: index + 1 msgid "Delete command {0}" msgstr "{0} numaralı komutu sil" @@ -9382,6 +9386,10 @@ msgstr "2027 ortası" msgid "Mine" msgstr "Benim" +msgctxt "workspaces I created" +msgid "Mine" +msgstr "Benimkiler" + msgid "Minimal reasoning effort" msgstr "En az akıl yürütme çabası" @@ -9934,6 +9942,9 @@ msgstr "Henüz ajan yok." msgid "No API keys yet." msgstr "Henüz API anahtarı yok." +msgid "No archived workspaces" +msgstr "Arşivlenmiş çalışma alanı yok" + msgid "No assignee" msgstr "Atanan yok" @@ -9982,6 +9993,9 @@ msgstr "Chrome, Brave, Arc veya başka bir Chromium tarayıcısı bulunamadı." msgid "No Claude or Codex usage found on this machine yet. Join now and you'll appear once you've used an agent." msgstr "Bu makinede henüz Claude veya Codex kullanımı bulunamadı. Şimdi katılın; bir ajan kullandığınızda listede görüneceksiniz." +msgid "No cloud workspaces match these filters" +msgstr "Bu filtrelerle eşleşen bulut çalışma alanı yok" + msgid "No cloud workspaces yet" msgstr "Henüz bulut çalışma alanı yok" @@ -10566,8 +10580,8 @@ msgstr "Burada bir şey yok." msgid "Nothing installed yet." msgstr "Henüz kurulu bir şey yok." -msgid "Nothing is running. Remove it and create a new one." -msgstr "Hiçbir şey çalışmıyor. Bunu kaldırıp yeni bir tane oluşturun." +msgid "Nothing is running. Archive it and create a new one." +msgstr "Hiçbir şey çalışmıyor. Bunu arşivleyip yeni bir tane oluşturun." msgid "Nothing lives at {pathname}." msgstr "{pathname} yolunda bir şey yok." @@ -12242,6 +12256,7 @@ msgid "Pull request" msgstr "Pull request" #. placeholder {0}: data.number +#. placeholder {0}: pullRequest.number #. placeholder {0}: pullRequest.prNumber msgid "Pull request #{0}" msgstr "Pull request #{0}" @@ -15000,8 +15015,8 @@ msgstr "Adım {step}: {name}" msgid "Still can't reach the host." msgstr "Host'a hâlâ ulaşılamıyor." -msgid "Still trying. If it keeps failing, remove it and create a new one." -msgstr "Hâlâ deneniyor. Başarısız olmaya devam ederse bunu kaldırıp yeni bir tane oluşturun." +msgid "Still trying. If it keeps failing, archive it and create a new one." +msgstr "Hâlâ deneniyor. Başarısız olmaya devam ederse bunu arşivleyip yeni bir tane oluşturun." msgid "Still working — large repos can take a few minutes. You can leave; the workspace will appear on Home when it's ready." msgstr "Hâlâ çalışıyor — büyük depolar birkaç dakika sürebilir. Ayrılabilirsiniz; çalışma alanı hazır olduğunda Ana sayfada görünecek." @@ -16772,6 +16787,9 @@ msgstr "Ses yazıya dökülemiyor" msgid "Unarchive" msgstr "Arşivden çıkar" +msgid "Unarchive failed" +msgstr "Arşivden çıkarılamadı" + msgid "Unarchive workspace" msgstr "Çalışma alanını arşivden çıkar" diff --git a/packages/i18n/locales/vi/messages.po b/packages/i18n/locales/vi/messages.po index 56ea7ddce3d..68d7452d106 100644 --- a/packages/i18n/locales/vi/messages.po +++ b/packages/i18n/locales/vi/messages.po @@ -1526,6 +1526,10 @@ msgstr "Kích hoạt, mức độ sử dụng sau khi bắt đầu dùng, và li msgid "Active" msgstr "Đang hoạt động" +msgctxt "cloud workspace status" +msgid "Active" +msgstr "Đang hoạt động" + msgctxt "presence" msgid "Active" msgstr "Đang hoạt động" @@ -2316,6 +2320,9 @@ msgstr "Lưu trữ" msgid "Archive environment" msgstr "Lưu trữ environment" +msgid "Archive failed" +msgstr "Lưu trữ thất bại" + msgid "Archive workspace" msgstr "Lưu trữ workspace" @@ -2323,6 +2330,7 @@ msgid "Archived" msgstr "Đã lưu trữ" #. placeholder {0}: formatCompactRelativeTime(archivedAt, now) +#. placeholder {0}: formatCompactRelativeTime(cloud.deletedAt) msgid "Archived · {0}" msgstr "Đã lưu trữ · {0}" @@ -5046,6 +5054,9 @@ msgstr "Người tạo" msgid "Created by <0/> · {createdAgo}" msgstr "Tạo bởi <0/> · {createdAgo}" +msgid "Created by me" +msgstr "Do tôi tạo" + msgid "Created team \"{trimmedName}\"" msgstr "Đã tạo nhóm \"{trimmedName}\"" @@ -5296,10 +5307,6 @@ msgstr "Xóa" msgid "Delete \"{0}\"? This removes its worktree from the host." msgstr "Xóa \"{0}\"? Thao tác này gỡ worktree của nó khỏi host." -#. placeholder {0}: target.name -msgid "Delete \"{0}\"? This shuts down its sandbox and everything in it." -msgstr "Xóa \"{0}\"? Thao tác này tắt sandbox của nó và mọi thứ bên trong." - msgid "Delete \"{basename}\"?" msgstr "Xóa \"{basename}\"?" @@ -5347,9 +5354,6 @@ msgstr "Xóa tự động hóa" msgid "Delete automation?" msgstr "Xóa tự động hóa?" -msgid "Delete cloud workspace" -msgstr "Xóa workspace cloud" - #. placeholder {0}: index + 1 msgid "Delete command {0}" msgstr "Xóa lệnh {0}" @@ -9382,6 +9386,10 @@ msgstr "Giữa 2027" msgid "Mine" msgstr "Của tôi" +msgctxt "workspaces I created" +msgid "Mine" +msgstr "Của tôi" + msgid "Minimal reasoning effort" msgstr "Mức suy luận tối thiểu" @@ -9934,6 +9942,9 @@ msgstr "Chưa có agent nào." msgid "No API keys yet." msgstr "Chưa có khóa API nào." +msgid "No archived workspaces" +msgstr "Không có workspace nào đã lưu trữ" + msgid "No assignee" msgstr "Không có người phụ trách" @@ -9982,6 +9993,9 @@ msgstr "Không tìm thấy Chrome, Brave, Arc hay trình duyệt Chromium nào k msgid "No Claude or Codex usage found on this machine yet. Join now and you'll appear once you've used an agent." msgstr "Chưa tìm thấy mức sử dụng Claude hay Codex nào trên máy này. Hãy tham gia ngay và bạn sẽ xuất hiện khi đã dùng một agent." +msgid "No cloud workspaces match these filters" +msgstr "Không có workspace cloud nào khớp với các bộ lọc này" + msgid "No cloud workspaces yet" msgstr "Chưa có workspace cloud nào" @@ -10566,8 +10580,8 @@ msgstr "Không có gì ở đây." msgid "Nothing installed yet." msgstr "Chưa cài gì cả." -msgid "Nothing is running. Remove it and create a new one." -msgstr "Không có gì đang chạy. Hãy gỡ nó và tạo cái mới." +msgid "Nothing is running. Archive it and create a new one." +msgstr "Không có gì đang chạy. Hãy lưu trữ nó và tạo cái mới." msgid "Nothing lives at {pathname}." msgstr "Không có gì ở {pathname}." @@ -12242,6 +12256,7 @@ msgid "Pull request" msgstr "Pull request" #. placeholder {0}: data.number +#. placeholder {0}: pullRequest.number #. placeholder {0}: pullRequest.prNumber msgid "Pull request #{0}" msgstr "Pull request #{0}" @@ -15000,8 +15015,8 @@ msgstr "Bước {step}: {name}" msgid "Still can't reach the host." msgstr "Vẫn không thể kết nối tới host." -msgid "Still trying. If it keeps failing, remove it and create a new one." -msgstr "Vẫn đang thử. Nếu tiếp tục lỗi, hãy gỡ nó và tạo cái mới." +msgid "Still trying. If it keeps failing, archive it and create a new one." +msgstr "Vẫn đang thử. Nếu tiếp tục lỗi, hãy lưu trữ nó và tạo cái mới." msgid "Still working — large repos can take a few minutes. You can leave; the workspace will appear on Home when it's ready." msgstr "Vẫn đang xử lý — repo lớn có thể mất vài phút. Bạn có thể rời đi; workspace sẽ xuất hiện ở Trang chủ khi sẵn sàng." @@ -16772,6 +16787,9 @@ msgstr "Không thể chuyển âm thanh thành văn bản" msgid "Unarchive" msgstr "Bỏ lưu trữ" +msgid "Unarchive failed" +msgstr "Bỏ lưu trữ thất bại" + msgid "Unarchive workspace" msgstr "Bỏ lưu trữ workspace" diff --git a/packages/i18n/locales/zh-CN/messages.po b/packages/i18n/locales/zh-CN/messages.po index eab452697fa..8750bfe58fa 100644 --- a/packages/i18n/locales/zh-CN/messages.po +++ b/packages/i18n/locales/zh-CN/messages.po @@ -1526,6 +1526,10 @@ msgstr "激活情况、进来之后的使用量,以及他们是否会回来。 msgid "Active" msgstr "启用中" +msgctxt "cloud workspace status" +msgid "Active" +msgstr "活跃" + msgctxt "presence" msgid "Active" msgstr "活跃" @@ -2316,6 +2320,9 @@ msgstr "归档" msgid "Archive environment" msgstr "归档环境" +msgid "Archive failed" +msgstr "归档失败" + msgid "Archive workspace" msgstr "归档工作区" @@ -2323,6 +2330,7 @@ msgid "Archived" msgstr "已归档" #. placeholder {0}: formatCompactRelativeTime(archivedAt, now) +#. placeholder {0}: formatCompactRelativeTime(cloud.deletedAt) msgid "Archived · {0}" msgstr "已归档 · {0}" @@ -5046,6 +5054,9 @@ msgstr "创建者" msgid "Created by <0/> · {createdAgo}" msgstr "创建者 <0/> · {createdAgo}" +msgid "Created by me" +msgstr "我创建的" + msgid "Created team \"{trimmedName}\"" msgstr "已创建团队“{trimmedName}”" @@ -5296,10 +5307,6 @@ msgstr "删除" msgid "Delete \"{0}\"? This removes its worktree from the host." msgstr "删除“{0}”?这会从主机上移除它的worktree。" -#. placeholder {0}: target.name -msgid "Delete \"{0}\"? This shuts down its sandbox and everything in it." -msgstr "删除“{0}”?这会关闭它的沙箱以及其中的一切。" - msgid "Delete \"{basename}\"?" msgstr "删除“{basename}”?" @@ -5347,9 +5354,6 @@ msgstr "删除自动化" msgid "Delete automation?" msgstr "删除自动化?" -msgid "Delete cloud workspace" -msgstr "删除云端工作区" - #. placeholder {0}: index + 1 msgid "Delete command {0}" msgstr "删除命令{0}" @@ -9382,6 +9386,10 @@ msgstr "2027年中" msgid "Mine" msgstr "我的" +msgctxt "workspaces I created" +msgid "Mine" +msgstr "我的" + msgid "Minimal reasoning effort" msgstr "最少的推理投入" @@ -9934,6 +9942,9 @@ msgstr "暂无智能体。" msgid "No API keys yet." msgstr "暂无API密钥。" +msgid "No archived workspaces" +msgstr "没有已归档的工作区" + msgid "No assignee" msgstr "无负责人" @@ -9982,6 +9993,9 @@ msgstr "未找到Chrome、Brave、Arc或其他Chromium浏览器。" msgid "No Claude or Codex usage found on this machine yet. Join now and you'll appear once you've used an agent." msgstr "这台机器上还没有发现Claude或Codex的用量。现在加入,用过智能体后你就会出现在榜单上。" +msgid "No cloud workspaces match these filters" +msgstr "没有符合这些筛选条件的云端工作区" + msgid "No cloud workspaces yet" msgstr "暂无云端工作区" @@ -10566,8 +10580,8 @@ msgstr "这里没有内容。" msgid "Nothing installed yet." msgstr "还没有安装任何插件。" -msgid "Nothing is running. Remove it and create a new one." -msgstr "没有任何东西在运行。移除它,再建一个。" +msgid "Nothing is running. Archive it and create a new one." +msgstr "没有任何东西在运行。归档它,再建一个。" msgid "Nothing lives at {pathname}." msgstr "{pathname}下没有任何内容。" @@ -12242,6 +12256,7 @@ msgid "Pull request" msgstr "拉取请求" #. placeholder {0}: data.number +#. placeholder {0}: pullRequest.number #. placeholder {0}: pullRequest.prNumber msgid "Pull request #{0}" msgstr "拉取请求#{0}" @@ -15000,8 +15015,8 @@ msgstr "第{step}步:{name}" msgid "Still can't reach the host." msgstr "仍然无法连接到主机。" -msgid "Still trying. If it keeps failing, remove it and create a new one." -msgstr "仍在重试。如果一直失败,移除它,再建一个。" +msgid "Still trying. If it keeps failing, archive it and create a new one." +msgstr "仍在重试。如果一直失败,归档它,再建一个。" msgid "Still working — large repos can take a few minutes. You can leave; the workspace will appear on Home when it's ready." msgstr "仍在处理 — 大仓库可能需要几分钟。你可以离开;工作区就绪后会出现在首页。" @@ -16772,6 +16787,9 @@ msgstr "无法转写音频" msgid "Unarchive" msgstr "取消归档" +msgid "Unarchive failed" +msgstr "取消归档失败" + msgid "Unarchive workspace" msgstr "取消归档工作区" diff --git a/packages/i18n/locales/zh-TW/messages.po b/packages/i18n/locales/zh-TW/messages.po index c51af39886b..56b35115d5d 100644 --- a/packages/i18n/locales/zh-TW/messages.po +++ b/packages/i18n/locales/zh-TW/messages.po @@ -1526,6 +1526,10 @@ msgstr "啟用情況、進來之後的使用量,以及他們是否會回來。 msgid "Active" msgstr "啟用中" +msgctxt "cloud workspace status" +msgid "Active" +msgstr "使用中" + msgctxt "presence" msgid "Active" msgstr "活躍" @@ -2316,6 +2320,9 @@ msgstr "歸檔" msgid "Archive environment" msgstr "封存環境" +msgid "Archive failed" +msgstr "封存失敗" + msgid "Archive workspace" msgstr "封存工作區" @@ -2323,6 +2330,7 @@ msgid "Archived" msgstr "已歸檔" #. placeholder {0}: formatCompactRelativeTime(archivedAt, now) +#. placeholder {0}: formatCompactRelativeTime(cloud.deletedAt) msgid "Archived · {0}" msgstr "已封存 · {0}" @@ -5046,6 +5054,9 @@ msgstr "建立者" msgid "Created by <0/> · {createdAgo}" msgstr "建立者 <0/> · {createdAgo}" +msgid "Created by me" +msgstr "我建立的" + msgid "Created team \"{trimmedName}\"" msgstr "已建立團隊「{trimmedName}」" @@ -5296,10 +5307,6 @@ msgstr "刪除" msgid "Delete \"{0}\"? This removes its worktree from the host." msgstr "刪除「{0}」?這會從主機上移除它的worktree。" -#. placeholder {0}: target.name -msgid "Delete \"{0}\"? This shuts down its sandbox and everything in it." -msgstr "刪除「{0}」?這會關閉它的沙箱以及其中的一切。" - msgid "Delete \"{basename}\"?" msgstr "刪除「{basename}」?" @@ -5347,9 +5354,6 @@ msgstr "刪除自動化" msgid "Delete automation?" msgstr "刪除自動化?" -msgid "Delete cloud workspace" -msgstr "刪除雲端工作區" - #. placeholder {0}: index + 1 msgid "Delete command {0}" msgstr "刪除命令{0}" @@ -9382,6 +9386,10 @@ msgstr "2027年中" msgid "Mine" msgstr "我的" +msgctxt "workspaces I created" +msgid "Mine" +msgstr "我的" + msgid "Minimal reasoning effort" msgstr "最少的推理投入" @@ -9934,6 +9942,9 @@ msgstr "暫無代理程式。" msgid "No API keys yet." msgstr "暫無API金鑰。" +msgid "No archived workspaces" +msgstr "沒有已封存的工作區" + msgid "No assignee" msgstr "無負責人" @@ -9982,6 +9993,9 @@ msgstr "未找到Chrome、Brave、Arc或其他Chromium瀏覽器。" msgid "No Claude or Codex usage found on this machine yet. Join now and you'll appear once you've used an agent." msgstr "這臺機器上還沒有發現Claude或Codex的用量。現在加入,用過代理程式後你就會出現在榜單上。" +msgid "No cloud workspaces match these filters" +msgstr "沒有符合這些篩選條件的雲端工作區" + msgid "No cloud workspaces yet" msgstr "暫無雲端工作區" @@ -10566,8 +10580,8 @@ msgstr "這裡沒有內容。" msgid "Nothing installed yet." msgstr "還沒有安裝任何外掛。" -msgid "Nothing is running. Remove it and create a new one." -msgstr "沒有任何東西在執行。移除它,再建一個。" +msgid "Nothing is running. Archive it and create a new one." +msgstr "沒有任何東西在執行。封存它,再建一個。" msgid "Nothing lives at {pathname}." msgstr "{pathname}下沒有任何內容。" @@ -12242,6 +12256,7 @@ msgid "Pull request" msgstr "拉取請求" #. placeholder {0}: data.number +#. placeholder {0}: pullRequest.number #. placeholder {0}: pullRequest.prNumber msgid "Pull request #{0}" msgstr "拉取請求#{0}" @@ -15000,8 +15015,8 @@ msgstr "第{step}步:{name}" msgid "Still can't reach the host." msgstr "仍然無法連線到主機。" -msgid "Still trying. If it keeps failing, remove it and create a new one." -msgstr "仍在重試。如果一直失敗,移除它,再建一個。" +msgid "Still trying. If it keeps failing, archive it and create a new one." +msgstr "仍在重試。如果一直失敗,封存它,再建一個。" msgid "Still working — large repos can take a few minutes. You can leave; the workspace will appear on Home when it's ready." msgstr "仍在處理 — 大儲存庫可能需要幾分鐘。你可以離開;工作區就緒後會出現在首頁。" @@ -16772,6 +16787,9 @@ msgstr "無法轉寫音訊" msgid "Unarchive" msgstr "取消封存" +msgid "Unarchive failed" +msgstr "取消封存失敗" + msgid "Unarchive workspace" msgstr "取消封存工作區" diff --git a/packages/i18n/src/format/format.test.ts b/packages/i18n/src/format/format.test.ts index 595ea625d9b..a5c797061db 100644 --- a/packages/i18n/src/format/format.test.ts +++ b/packages/i18n/src/format/format.test.ts @@ -9,6 +9,7 @@ import { formatNumber, formatPercent, formatPrice, + formatRelativePeriod, formatRelativeTime, getActiveLocale, } from "./index"; @@ -159,3 +160,18 @@ describe("formatRelativeTime", () => { } }); }); + +describe("formatRelativePeriod", () => { + test("names recent periods the way a list heading would", () => { + expect(formatRelativePeriod({ unit: "day", count: 0 }, "en")).toBe("Today"); + expect(formatRelativePeriod({ unit: "day", count: 1 }, "en")).toBe( + "Yesterday", + ); + expect(formatRelativePeriod({ unit: "day", count: 3 }, "en")).toBe( + "3 days ago", + ); + expect(formatRelativePeriod({ unit: "week", count: 1 }, "en")).toBe( + "Last week", + ); + }); +}); diff --git a/packages/i18n/src/format/index.ts b/packages/i18n/src/format/index.ts index 617100dacd3..d4a94ddd5ac 100644 --- a/packages/i18n/src/format/index.ts +++ b/packages/i18n/src/format/index.ts @@ -231,3 +231,13 @@ export function formatAge( ? formatUnit(locale, unit, count, "short") : narrow; } + +export function formatRelativePeriod( + { unit, count }: { unit: "day" | "week" | "month" | "year"; count: number }, + locale = getActiveLocale(), +): string { + const label = new Intl.RelativeTimeFormat(locale, { + numeric: "auto", + }).format(-count, unit); + return label.charAt(0).toLocaleUpperCase(locale) + label.slice(1); +} diff --git a/packages/i18n/src/hooks/useFormat/useFormat.ts b/packages/i18n/src/hooks/useFormat/useFormat.ts index 18e8c3ff0ad..ad7d82fef4c 100644 --- a/packages/i18n/src/hooks/useFormat/useFormat.ts +++ b/packages/i18n/src/hooks/useFormat/useFormat.ts @@ -57,6 +57,10 @@ export function useFormat() { format.formatCompactRelativeTime(value, now, locale), formatAge: (value: Date | number, now?: Date | number) => format.formatAge(value, now, locale), + formatRelativePeriod: (period: { + unit: "day" | "week" | "month" | "year"; + count: number; + }) => format.formatRelativePeriod(period, locale), }), [locale], ); diff --git a/packages/shared/package.json b/packages/shared/package.json index 16dc31bfb9d..2218ebf3411 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -353,6 +353,10 @@ "types": "./src/plugins/manifest-schema.ts", "default": "./src/plugins/manifest-schema.ts" }, + "./cloud-workspace-groups": { + "types": "./src/cloud-workspace-groups.ts", + "default": "./src/cloud-workspace-groups.ts" + }, "./workspace-activity": { "types": "./src/workspace-activity.ts", "default": "./src/workspace-activity.ts" diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/utils/groupCloudWorkspaces/groupCloudWorkspaces.test.ts b/packages/shared/src/cloud-workspace-groups.test.ts similarity index 84% rename from apps/desktop/src/renderer/routes/_authenticated/_dashboard/utils/groupCloudWorkspaces/groupCloudWorkspaces.test.ts rename to packages/shared/src/cloud-workspace-groups.test.ts index 605ee21b279..29a91c59f47 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/utils/groupCloudWorkspaces/groupCloudWorkspaces.test.ts +++ b/packages/shared/src/cloud-workspace-groups.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test"; import { groupCloudWorkspaces, groupCloudWorkspacesByTime, -} from "./groupCloudWorkspaces"; +} from "./cloud-workspace-groups"; describe("groupCloudWorkspaces", () => { test("groups each box under whoever is in it now, else its creator", () => { @@ -188,3 +188,37 @@ describe("groupCloudWorkspacesByTime", () => { expect(group?.period).toEqual({ unit: "week", count: 1 }); }); }); + +describe("groupCloudWorkspacesByTime with at", () => { + test("groups and orders by the given time instead of the sort's", () => { + const now = new Date("2026-09-30T12:00:00"); + const groups = groupCloudWorkspacesByTime({ + workspaces: [ + { + id: "archived-yesterday", + createdAt: new Date("2026-09-30T09:00:00"), + agentStatusAt: null, + deletedAt: new Date("2026-09-29T10:00:00"), + }, + { + id: "archived-today", + createdAt: new Date("2026-09-01T09:00:00"), + agentStatusAt: null, + deletedAt: new Date("2026-09-30T11:00:00"), + }, + ], + now, + sort: "activity", + at: (workspace) => workspace.deletedAt, + }); + expect( + groups.map(({ period, workspaces }) => [ + period.count, + workspaces.map((workspace) => workspace.id), + ]), + ).toEqual([ + [0, ["archived-today"]], + [1, ["archived-yesterday"]], + ]); + }); +}); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/utils/groupCloudWorkspaces/groupCloudWorkspaces.ts b/packages/shared/src/cloud-workspace-groups.ts similarity index 73% rename from apps/desktop/src/renderer/routes/_authenticated/_dashboard/utils/groupCloudWorkspaces/groupCloudWorkspaces.ts rename to packages/shared/src/cloud-workspace-groups.ts index 2d1f8db0718..1c05509bdaf 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/utils/groupCloudWorkspaces/groupCloudWorkspaces.ts +++ b/packages/shared/src/cloud-workspace-groups.ts @@ -1,28 +1,29 @@ -import type { CloudWorkspaceRow } from "renderer/hooks/useCloudWorkspaces"; -import { sortByLastAgentMessage } from "renderer/routes/_authenticated/_dashboard/utils/buildCloudSidebar"; - -type GroupedWorkspace = Pick< - CloudWorkspaceRow, - "id" | "createdBy" | "presence" | "agentStatusAt" | "createdAt" ->; -type Person = NonNullable; - export type CloudWorkspaceSort = "activity" | "created"; -interface CloudWorkspaceGroup { - person: Person | null; - workspaces: Workspace[]; -} - -type SortableWorkspace = Pick; - export interface CloudWorkspacePeriod { unit: "day" | "week" | "month" | "year"; count: number; } +interface Person { + userId: string; + name: string; + image: string | null; +} + +type SortableWorkspace = { agentStatusAt: Date | null; createdAt: Date }; + +type GroupedWorkspace = SortableWorkspace & { + id: string; + createdBy: Person | null; + presence: (Person & { lastSeenAt: Date })[]; +}; + const DAY_MS = 24 * 60 * 60 * 1000; +const lastAgentMessageAt = (workspace: SortableWorkspace) => + (workspace.agentStatusAt ?? workspace.createdAt).getTime(); + const sortedAt = (workspace: SortableWorkspace, sort: CloudWorkspaceSort) => sort === "created" ? workspace.createdAt @@ -42,23 +43,47 @@ function periodOf(date: Date, now: Date): CloudWorkspacePeriod { return { unit: "year", count: Math.floor(days / 365) }; } +export function sortByLastAgentMessage( + workspaces: Workspace[], +): Workspace[] { + return [...workspaces].sort( + (left, right) => lastAgentMessageAt(right) - lastAgentMessageAt(left), + ); +} + +export function sortCloudWorkspaces( + workspaces: Workspace[], + sort: CloudWorkspaceSort, +): Workspace[] { + return sort === "created" + ? [...workspaces].sort( + (left, right) => right.createdAt.getTime() - left.createdAt.getTime(), + ) + : sortByLastAgentMessage(workspaces); +} + export function groupCloudWorkspacesByTime< Workspace extends SortableWorkspace, >({ workspaces, now, sort, + at = (workspace) => sortedAt(workspace, sort), }: { workspaces: Workspace[]; now: Date; sort: CloudWorkspaceSort; + at?: (workspace: Workspace) => Date; }): { period: CloudWorkspacePeriod; workspaces: Workspace[] }[] { const groups = new Map< string, { period: CloudWorkspacePeriod; workspaces: Workspace[] } >(); - for (const workspace of sortCloudWorkspaces(workspaces, sort)) { - const period = periodOf(sortedAt(workspace, sort), now); + const ordered = [...workspaces].sort( + (left, right) => at(right).getTime() - at(left).getTime(), + ); + for (const workspace of ordered) { + const period = periodOf(at(workspace), now); const key = `${period.unit}:${period.count}`; const group = groups.get(key) ?? { period, workspaces: [] }; group.workspaces.push(workspace); @@ -67,17 +92,6 @@ export function groupCloudWorkspacesByTime< return [...groups.values()]; } -export function sortCloudWorkspaces( - workspaces: Workspace[], - sort: CloudWorkspaceSort, -): Workspace[] { - return sort === "created" - ? [...workspaces].sort( - (left, right) => right.createdAt.getTime() - left.createdAt.getTime(), - ) - : sortByLastAgentMessage(workspaces); -} - export function groupCloudWorkspaces({ workspaces, userId, @@ -90,10 +104,12 @@ export function groupCloudWorkspaces({ now: Date; activeWithinMs: number; sort?: CloudWorkspaceSort; -}): CloudWorkspaceGroup[] { - const groups = new Map>(); - const ordered = sortCloudWorkspaces(workspaces, sort); - for (const workspace of ordered) { +}): { person: Person | null; workspaces: Workspace[] }[] { + const groups = new Map< + string, + { person: Person | null; workspaces: Workspace[] } + >(); + for (const workspace of sortCloudWorkspaces(workspaces, sort)) { const inItNow = workspace.presence.find( (person) => now.getTime() - person.lastSeenAt.getTime() < activeWithinMs, ); diff --git a/packages/trpc/src/router/cloud-workspace/cloud-workspace.ts b/packages/trpc/src/router/cloud-workspace/cloud-workspace.ts index b9679440c2c..2973a80109f 100644 --- a/packages/trpc/src/router/cloud-workspace/cloud-workspace.ts +++ b/packages/trpc/src/router/cloud-workspace/cloud-workspace.ts @@ -15,7 +15,7 @@ import { } from "@superset/shared/cloud-agent-launch"; import type { TRPCRouterRecord } from "@trpc/server"; import { TRPCError } from "@trpc/server"; -import { and, asc, desc, eq, inArray, ne, sql } from "drizzle-orm"; +import { and, asc, desc, eq, inArray, isNotNull, ne, sql } from "drizzle-orm"; import { z } from "zod"; import { assertCloudAccess, assertMember } from "../../lib/cloud-guards"; import { nudge } from "../../lib/realtime"; @@ -222,7 +222,11 @@ export const cloudWorkspaceRouter = { // the client renders provisioning and failed rows off // `status` rather than being told they don't exist yet. input.archived - ? eq(cloudWorkspaces.status, "deleted") + ? and( + eq(cloudWorkspaces.status, "deleted"), + // Rows deleted before archiving existed have no deletedAt and no box. + isNotNull(cloudWorkspaces.deletedAt), + ) : ne(cloudWorkspaces.status, "deleted"), ), )