From e13a2401225554855d8594e2f5ff798427cdf556 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 27 Aug 2026 05:16:31 +0200 Subject: [PATCH 1/8] feat(mobile): add glanceable agents snapshot contract --- apps/mobile/src/app/(app)/_layout.tsx | 4 + apps/mobile/src/i18n/locales/en.json | 15 + apps/mobile/src/lib/auth/auth-context.tsx | 11 + .../mobile/src/lib/glanceable/cleanup.test.ts | 182 ++++++++++++ apps/mobile/src/lib/glanceable/cleanup.ts | 127 +++++++++ apps/mobile/src/lib/glanceable/mount.tsx | 107 +++++++ apps/mobile/src/lib/glanceable/open-agents.ts | 12 + apps/mobile/src/lib/glanceable/org-fence.ts | 42 +++ .../mobile/src/lib/glanceable/persist.test.ts | 116 ++++++++ apps/mobile/src/lib/glanceable/persist.ts | 139 ++++++++++ .../src/lib/glanceable/presentation.test.ts | 108 ++++++++ .../mobile/src/lib/glanceable/presentation.ts | 107 +++++++ .../src/lib/glanceable/publisher.test.ts | 243 ++++++++++++++++ apps/mobile/src/lib/glanceable/publisher.ts | 260 ++++++++++++++++++ .../src/lib/glanceable/sink-registry.ts | 57 ++++ apps/mobile/src/lib/organization-context.tsx | 29 +- apps/mobile/vitest.pure.config.ts | 1 + packages/app-shared/package.json | 3 +- .../src/glanceable-agents-snapshot.test.ts | 210 ++++++++++++++ .../src/glanceable-agents-snapshot.ts | 187 +++++++++++++ 20 files changed, 1951 insertions(+), 9 deletions(-) create mode 100644 apps/mobile/src/lib/glanceable/cleanup.test.ts create mode 100644 apps/mobile/src/lib/glanceable/cleanup.ts create mode 100644 apps/mobile/src/lib/glanceable/mount.tsx create mode 100644 apps/mobile/src/lib/glanceable/open-agents.ts create mode 100644 apps/mobile/src/lib/glanceable/org-fence.ts create mode 100644 apps/mobile/src/lib/glanceable/persist.test.ts create mode 100644 apps/mobile/src/lib/glanceable/persist.ts create mode 100644 apps/mobile/src/lib/glanceable/presentation.test.ts create mode 100644 apps/mobile/src/lib/glanceable/presentation.ts create mode 100644 apps/mobile/src/lib/glanceable/publisher.test.ts create mode 100644 apps/mobile/src/lib/glanceable/publisher.ts create mode 100644 apps/mobile/src/lib/glanceable/sink-registry.ts create mode 100644 packages/app-shared/src/glanceable-agents-snapshot.test.ts create mode 100644 packages/app-shared/src/glanceable-agents-snapshot.ts diff --git a/apps/mobile/src/app/(app)/_layout.tsx b/apps/mobile/src/app/(app)/_layout.tsx index f0bc6944c5..385bc18510 100644 --- a/apps/mobile/src/app/(app)/_layout.tsx +++ b/apps/mobile/src/app/(app)/_layout.tsx @@ -9,6 +9,8 @@ import { SharePayloadNavigator } from '@/components/share/share-payload-navigato import { privacyScreenLayout } from '@/components/privacy-cover-overlay'; import { ActiveSessionsLiveSyncMount } from '@/lib/active-sessions-live-sync-mount'; import { attemptLogoutReconciliation } from '@/lib/auth/logout-reconciliation'; +import { GlanceablePublisherMount } from '@/lib/glanceable/mount'; +import { useGlanceableOrgFence } from '@/lib/glanceable/org-fence'; import { attemptPushRegistrationReconciliation, subscribeToPushTokenRotation, @@ -108,10 +110,12 @@ export default function AppLayout() { const colors = useThemeColors(); const { fullSheetDetent } = useFormSheetDetents(); useSecurityLifecycleInvalidation(); + useGlanceableOrgFence(); return ( + diff --git a/apps/mobile/src/i18n/locales/en.json b/apps/mobile/src/i18n/locales/en.json index ad2061678c..b0326cb45e 100644 --- a/apps/mobile/src/i18n/locales/en.json +++ b/apps/mobile/src/i18n/locales/en.json @@ -3149,5 +3149,20 @@ "xhigh": "Extra High", "max": "Max" } + }, + "glanceable": { + "waiting": "Updating agents", + "empty": "No work in progress", + "stale": "Can't update now", + "expired": "Status expired", + "signedOut": "Sign in to see agents", + "privacy": "Agents hidden", + "openAgents": "Open agents", + "running": "Running", + "needsInput": "Needs input", + "reconnecting": "Reconnecting", + "channelName": "Active agents", + "activityKitDisabledTitle": "Live Activities are off", + "activityKitDisabledBody": "Turn on Live Activities in Settings to see Active Agents on the Lock Screen." } } diff --git a/apps/mobile/src/lib/auth/auth-context.tsx b/apps/mobile/src/lib/auth/auth-context.tsx index 94d8f8ca6a..bd25a10051 100644 --- a/apps/mobile/src/lib/auth/auth-context.tsx +++ b/apps/mobile/src/lib/auth/auth-context.tsx @@ -15,6 +15,7 @@ import { import { discardPostHog } from '@/lib/analytics/posthog'; import { resetAppsFlyerState, trackEvent } from '@/lib/appsflyer'; import { clearAccountBoundPendingDeepLink, setCurrentDeepLinkUserId } from '@/lib/deep-link-launch'; +import { writeSignedOutSnapshotAndEnd } from '@/lib/glanceable/cleanup'; import { deleteAccountMetadata } from '@/lib/auth/account-metadata-write'; import { runLogoutCleanup } from '@/lib/auth/logout-cleanup'; import { queryClient } from '@/lib/query-client'; @@ -204,6 +205,9 @@ export function AuthProvider({ children }: { readonly children: ReactNode }) { // session out (documented, correct FIFO semantics). await chainSave('auth-transition', async () => { bumpAuthEpoch(); + // Blank the prior account's glanceable surface before any credential + // persist, so a direct account switch never shows the previous account. + writeSignedOutSnapshotAndEnd(); setAuthEpoch(currentAuthEpoch()); // Bind the pending deep-link slot to the new user id at the same // place the auth epoch advances, so a destination captured while this @@ -232,6 +236,10 @@ export function AuthProvider({ children }: { readonly children: ReactNode }) { setSignOutActive(false); trackEvent('login'); resetPurchaseErrorToastDedup(); + // A direct account switch must not keep the prior account's query + // cache: the org list is keyed account-independently, so a stale list + // would otherwise drive a false lost-org blank in the org fence. + queryClient.clear(); setToken(tokenValue); // A direct account switch must not keep the prior account's session // state: trusted hosts, image confirms, media caches, temp copies. @@ -264,6 +272,9 @@ export function AuthProvider({ children }: { readonly children: ReactNode }) { // the read-cache mount unsubscribes and cannot resubscribe while the old // user id is still cached. setSignOutActive(true); + // Blank the glanceable surface synchronously, before the first await, so + // widgets/activities never outlive the session. + writeSignedOutSnapshotAndEnd(); // Drop an account-bound pending deep-link destination synchronously, // before the first await, so a different account signed in later in this // process cannot navigate to the previous account's destination. A diff --git a/apps/mobile/src/lib/glanceable/cleanup.test.ts b/apps/mobile/src/lib/glanceable/cleanup.test.ts new file mode 100644 index 0000000000..00693ce963 --- /dev/null +++ b/apps/mobile/src/lib/glanceable/cleanup.test.ts @@ -0,0 +1,182 @@ +import { afterEach, describe, expect, it } from 'vitest'; + +import { type GlanceableAgentsSnapshot } from '@kilocode/app-shared/glanceable-agents-snapshot'; + +import { + planOrgFenceAction, + republishLastSnapshotStale, + writePrivacySnapshotAndEnd, + writeSignedOutSnapshotAndEnd, +} from './cleanup'; +import { _resetGlanceablePersistForTests, _setLastGlanceableSnapshotForTests } from './persist'; +import { + type GlanceableSink, + registerGlanceableSink, + unregisterGlanceableSink, +} from './sink-registry'; + +type SinkCall = + | { type: 'publish'; snapshot: GlanceableAgentsSnapshot } + | { type: 'startOrUpdate'; snapshot: GlanceableAgentsSnapshot } + | { type: 'endImmediate' }; + +function makeSink() { + const calls: SinkCall[] = []; + const sink: GlanceableSink = { + publish(snapshot) { + calls.push({ type: 'publish', snapshot }); + }, + startOrUpdate(snapshot) { + calls.push({ type: 'startOrUpdate', snapshot }); + }, + endImmediate() { + calls.push({ type: 'endImmediate' }); + }, + }; + return { sink, calls }; +} + +function lastSnapshot(calls: SinkCall[]): GlanceableAgentsSnapshot { + const found = [...calls].toReversed().find(call => call.type === 'publish'); + if (found === undefined) { + throw new Error('no publish call'); + } + return found.snapshot; +} + +afterEach(() => { + _resetGlanceablePersistForTests(); +}); + +describe('cleanup', () => { + it('writes the signed-out snapshot before ending, and skips the terminal wait', () => { + const { sink, calls } = makeSink(); + registerGlanceableSink(sink); + try { + writeSignedOutSnapshotAndEnd(); + expect(calls.map(call => call.type)).toEqual(['publish', 'endImmediate']); + const snapshot = lastSnapshot(calls); + expect(snapshot.status).toBe('signed_out'); + expect(snapshot.running + snapshot.needsInput + snapshot.reconnecting).toBe(0); + } finally { + unregisterGlanceableSink(sink); + } + }); + + it('blanks to privacy on org switch', () => { + const { sink, calls } = makeSink(); + registerGlanceableSink(sink); + try { + writePrivacySnapshotAndEnd(); + const snapshot = lastSnapshot(calls); + expect(snapshot.status).toBe('privacy'); + expect(snapshot.running + snapshot.needsInput + snapshot.reconnecting).toBe(0); + } finally { + unregisterGlanceableSink(sink); + } + }); + + it('blanks to privacy only after a successful list misses the selection', () => { + expect( + planOrgFenceAction({ + organizationId: 'missing-org', + orgs: [{ organizationId: 'kept-org' }], + isLoading: false, + isError: false, + }) + ).toBe('privacy'); + expect( + planOrgFenceAction({ + organizationId: 'missing-org', + orgs: [{ organizationId: 'kept-org' }], + isLoading: true, + isError: false, + }) + ).toBe('none'); + expect( + planOrgFenceAction({ + organizationId: 'kept-org', + orgs: [{ organizationId: 'kept-org' }], + isLoading: false, + isError: false, + }) + ).toBe('none'); + expect( + planOrgFenceAction({ organizationId: null, orgs: [], isLoading: false, isError: false }) + ).toBe('none'); + // An offline or not-yet-fetched list (orgs undefined) is not a lost org. + expect( + planOrgFenceAction({ + organizationId: 'kept-org', + orgs: undefined, + isLoading: false, + isError: false, + }) + ).toBe('none'); + }); + + it('marks stale (keeps counts) when the org list errors', () => { + expect( + planOrgFenceAction({ + organizationId: 'kept-org', + orgs: undefined, + isLoading: false, + isError: true, + }) + ).toBe('stale'); + + const seeded: GlanceableAgentsSnapshot = { + schemaVersion: 1, + revision: 3, + updatedAt: '2026-08-27T00:00:00.000Z', + expiresAt: '2026-08-27T08:00:00.000Z', + scopeKey: 'deadbeef', + organizationBound: false, + status: 'happy', + running: 2, + needsInput: 1, + reconnecting: 0, + eligibleStartedAt: '2026-08-26T23:00:00.000Z', + }; + _setLastGlanceableSnapshotForTests(seeded); + + const { sink, calls } = makeSink(); + registerGlanceableSink(sink); + try { + republishLastSnapshotStale(); + const snapshot = lastSnapshot(calls); + expect(snapshot.status).toBe('stale'); + expect(snapshot.running).toBe(2); + expect(snapshot.needsInput).toBe(1); + expect(snapshot.revision).toBe(4); + } finally { + unregisterGlanceableSink(sink); + } + }); + + it('does not overwrite a terminal blank with a stale republish', () => { + const terminal: GlanceableAgentsSnapshot = { + schemaVersion: 1, + revision: 5, + updatedAt: '2026-08-27T00:00:00.000Z', + expiresAt: '2026-08-27T08:00:00.000Z', + scopeKey: 'terminal:privacy', + organizationBound: false, + status: 'privacy', + running: 0, + needsInput: 0, + reconnecting: 0, + eligibleStartedAt: null, + }; + _setLastGlanceableSnapshotForTests(terminal); + + const { sink, calls } = makeSink(); + registerGlanceableSink(sink); + try { + republishLastSnapshotStale(); + expect(calls).toEqual([]); + } finally { + unregisterGlanceableSink(sink); + } + }); +}); diff --git a/apps/mobile/src/lib/glanceable/cleanup.ts b/apps/mobile/src/lib/glanceable/cleanup.ts new file mode 100644 index 0000000000..57f985a4a2 --- /dev/null +++ b/apps/mobile/src/lib/glanceable/cleanup.ts @@ -0,0 +1,127 @@ +import { + GLANCEABLE_SNAPSHOT_EXPIRY_MS, + GLANCEABLE_SNAPSHOT_SCHEMA_VERSION, + type GlanceableAgentsSnapshot, +} from '@kilocode/app-shared/glanceable-agents-snapshot'; + +import { getLastGlanceableSnapshot } from './persist'; +import { getGlanceableSinks } from './sink-registry'; + +// Monotonic epoch bumped on every terminal blank (signed-out or privacy). The +// publisher captures it at construction and refuses to emit once it advances, +// so a live cache success after a blank can never republish or restart. +let terminalBlankEpoch = 0; + +/** Current terminal-blank epoch; the publisher compares it to its start value. */ +export function getTerminalBlankEpoch(): number { + return terminalBlankEpoch; +} + +/** + * Terminal blanking: signed-out and privacy states are written to every sink + * (publish) and then every sink ends immediately. The 8 s terminal window is + * skipped — logout, account switch, org switch, and confirmed lost org must + * blank at once. + */ + +export type GlanceableOrgFenceState = { + organizationId: string | null; + orgs: readonly { organizationId: string }[] | undefined; + isLoading: boolean; + isError: boolean; +}; + +export type GlanceableOrgFenceAction = 'privacy' | 'stale' | 'none'; + +/** Pure org-fence decision: lost org only after a successful list misses the selection. */ +export function planOrgFenceAction(state: GlanceableOrgFenceState): GlanceableOrgFenceAction { + if (state.isLoading) { + return 'none'; + } + if (state.isError) { + return 'stale'; + } + if ( + state.organizationId !== null && + state.orgs !== undefined && + !state.orgs.some(entry => entry.organizationId === state.organizationId) + ) { + return 'privacy'; + } + return 'none'; +} + +function buildTerminalSnapshot(status: 'signed_out' | 'privacy'): GlanceableAgentsSnapshot { + const previous = getLastGlanceableSnapshot(); + const now = Date.now(); + const updatedAt = new Date(now).toISOString(); + return { + schemaVersion: GLANCEABLE_SNAPSHOT_SCHEMA_VERSION, + revision: (previous?.revision ?? 0) + 1, + updatedAt, + expiresAt: new Date(now + GLANCEABLE_SNAPSHOT_EXPIRY_MS).toISOString(), + // A terminal scope never matches a real push scope, so a late push for the + // old account cannot resurrect the old surface. + scopeKey: `terminal:${status}`, + organizationBound: false, + status, + running: 0, + needsInput: 0, + reconnecting: 0, + eligibleStartedAt: null, + }; +} + +function writeTerminalAndEnd(status: 'signed_out' | 'privacy'): void { + // Arm the publisher gate before any sink writes, so a cache success that + // lands during this window can never emit for the torn-down session. + terminalBlankEpoch += 1; + const snapshot = buildTerminalSnapshot(status); + const sinks = getGlanceableSinks(); + // Write the snapshot first, then end: the surface shows the terminal copy + // before the native activity ends. + for (const sink of sinks) { + sink.publish(snapshot); + } + for (const sink of sinks) { + sink.endImmediate(); + } +} + +/** Blank on logout or direct account switch. */ +export function writeSignedOutSnapshotAndEnd(): void { + writeTerminalAndEnd('signed_out'); +} + +/** Blank on org switch or confirmed lost org. */ +export function writePrivacySnapshotAndEnd(): void { + writeTerminalAndEnd('privacy'); +} + +/** + * Republish the last snapshot with a stale status (keeps counts). Used by the + * org fence when the org list errors: stale, not lost-org. + */ +export function republishLastSnapshotStale(): void { + const previous = getLastGlanceableSnapshot(); + if (previous === null) { + return; + } + // A terminal blank must keep its status: a stale republish would replace the + // signed-out or privacy copy with "Can't update now". + if (previous.status === 'signed_out' || previous.status === 'privacy') { + return; + } + const now = Date.now(); + const updatedAt = new Date(now).toISOString(); + const snapshot: GlanceableAgentsSnapshot = { + ...previous, + revision: previous.revision + 1, + updatedAt, + expiresAt: new Date(now + GLANCEABLE_SNAPSHOT_EXPIRY_MS).toISOString(), + status: 'stale', + }; + for (const sink of getGlanceableSinks()) { + sink.publish(snapshot); + } +} diff --git a/apps/mobile/src/lib/glanceable/mount.tsx b/apps/mobile/src/lib/glanceable/mount.tsx new file mode 100644 index 0000000000..f50680f6a9 --- /dev/null +++ b/apps/mobile/src/lib/glanceable/mount.tsx @@ -0,0 +1,107 @@ +import { hashKey, useQueryClient } from '@tanstack/react-query'; +import { useEffect, useMemo, useState } from 'react'; + +import { + buildActiveSessionsTrayInput, + type CachedActiveSessionsData, +} from '@/lib/active-sessions-live'; +import { useAuth } from '@/lib/auth/auth-context'; +import { useCurrentUserId } from '@/lib/hooks/use-current-user-id'; +import { useOrganization } from '@/lib/organization-context'; +import { useTRPC } from '@/lib/trpc'; + +import { + getLastGlanceableSnapshot, + persistGlanceableSink, + restorePersistedGlanceable, +} from './persist'; +import { getTerminalBlankEpoch } from './cleanup'; +import { GlanceablePublisher } from './publisher'; +import { getGlanceableSinks, registerGlanceableSink } from './sink-registry'; + +// Register only the persist sink here; platform sinks register themselves from +// files their slices own. +registerGlanceableSink(persistGlanceableSink); + +/** + * React entry point for the glanceable publisher. Subscribes to the + * `activeSessions.list` tray cache (the same key the live-sync owner writes) + * and derives snapshots without fetching. A fresh publisher per signed-in + * context, mirroring `ActiveSessionsLiveSyncMount`. + */ +export function GlanceablePublisherMount(): null { + const trpc = useTRPC(); + const queryClient = useQueryClient(); + const { organizationId, isLoaded } = useOrganization(); + const { token } = useAuth(); + const { userId } = useCurrentUserId(); + + const input = useMemo(() => buildActiveSessionsTrayInput(organizationId), [organizationId]); + const queryKey = useMemo(() => trpc.activeSessions.list.queryKey(input), [trpc, input]); + const targetHash = useMemo(() => hashKey(queryKey), [queryKey]); + + const signedIn = token != null; + + // Populate the persisted last snapshot once so cleanup/org-fence can see it, + // and so the publisher below seeds its revision from the persisted value. + const [restored, setRestored] = useState(false); + useEffect(() => { + let cancelled = false; + const restore = async (): Promise => { + await restorePersistedGlanceable(); + if (!cancelled) { + setRestored(true); + } + }; + void restore(); + return () => { + cancelled = true; + }; + }, []); + + useEffect(() => { + if (!isLoaded || !signedIn || userId === undefined || !restored) { + return undefined; + } + + const publisher = new GlanceablePublisher({ + sinks: getGlanceableSinks(), + initial: getLastGlanceableSnapshot(), + terminalBlankEpoch: getTerminalBlankEpoch, + }); + const ctx = { userId, organizationId }; + + // Initial state: derive from the existing cache, or mark waiting while the + // first fetch is in flight. + const state = queryClient.getQueryState(queryKey); + const data = queryClient.getQueryData(queryKey); + if (data !== undefined) { + publisher.handleSessions(data.sessions, ctx); + } else if (state?.fetchStatus === 'fetching') { + publisher.handleFetchStarted(ctx); + } + + const unsubscribe = queryClient.getQueryCache().subscribe(event => { + if (event.type !== 'updated' || event.query.queryHash !== targetHash) { + return; + } + if (event.action.type === 'success') { + const next = queryClient.getQueryData(queryKey); + if (next !== undefined) { + publisher.handleSessions(next.sessions, ctx); + } + } else if (event.action.type === 'error') { + publisher.handleFetchError(ctx); + } else if (event.action.type === 'fetch') { + publisher.handleFetchStarted(ctx); + } + }); + + return () => { + unsubscribe(); + publisher.dispose(); + }; + }, [queryClient, queryKey, targetHash, isLoaded, signedIn, userId, organizationId, restored]); + + return null; +} diff --git a/apps/mobile/src/lib/glanceable/open-agents.ts b/apps/mobile/src/lib/glanceable/open-agents.ts new file mode 100644 index 0000000000..2c180b062a --- /dev/null +++ b/apps/mobile/src/lib/glanceable/open-agents.ts @@ -0,0 +1,12 @@ +import { setPendingDeepLink } from '@/lib/deep-link-launch'; + +/** + * The one safe Open agents action: stash the Agents tab destination through + * the existing pending-deep-link gate, which runs after auth and startup + * gates clear. `universal-link` always wins over a stale notification + * response, so a user tap takes precedence. Do not call `router.navigate` + * here — navigation must flow through the gate, not bypass it. + */ +export function openGlanceableAgents(): void { + setPendingDeepLink('/(app)/(tabs)/(2_agents)', 'universal-link'); +} diff --git a/apps/mobile/src/lib/glanceable/org-fence.ts b/apps/mobile/src/lib/glanceable/org-fence.ts new file mode 100644 index 0000000000..960a5562b1 --- /dev/null +++ b/apps/mobile/src/lib/glanceable/org-fence.ts @@ -0,0 +1,42 @@ +import { useQuery } from '@tanstack/react-query'; +import { useEffect } from 'react'; + +import { useAuth } from '@/lib/auth/auth-context'; +import { useOrganization } from '@/lib/organization-context'; +import { useTRPC } from '@/lib/trpc'; + +import { + planOrgFenceAction, + republishLastSnapshotStale, + writePrivacySnapshotAndEnd, +} from './cleanup'; +import { getLastGlanceableSnapshot } from './persist'; + +/** + * Org fence: a selected organization that is missing from a successful + * `organizations.list` is a confirmed lost org and blanks the surface. A + * loading or errored list never blanks; an error only marks the last snapshot + * stale. + */ +export function useGlanceableOrgFence(): void { + const trpc = useTRPC(); + const { token } = useAuth(); + const { organizationId } = useOrganization(); + const { + data: orgs, + isLoading, + isError, + } = useQuery({ + ...trpc.organizations.list.queryOptions(), + enabled: token != null, + }); + + useEffect(() => { + const action = planOrgFenceAction({ organizationId, orgs, isLoading, isError }); + if (action === 'privacy') { + writePrivacySnapshotAndEnd(); + } else if (action === 'stale' && getLastGlanceableSnapshot() !== null) { + republishLastSnapshotStale(); + } + }, [organizationId, orgs, isLoading, isError]); +} diff --git a/apps/mobile/src/lib/glanceable/persist.test.ts b/apps/mobile/src/lib/glanceable/persist.test.ts new file mode 100644 index 0000000000..86491fa7fe --- /dev/null +++ b/apps/mobile/src/lib/glanceable/persist.test.ts @@ -0,0 +1,116 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { + buildGlanceableSnapshot, + type GlanceableAgentsSnapshot, +} from '@kilocode/app-shared/glanceable-agents-snapshot'; + +import { + _resetGlanceablePersistForTests, + _setSecureStoreForTests, + getLastGlanceableSnapshot, + getLocalScopeKey, + persistGlanceableSink, + restorePersistedGlanceable, +} from './persist'; + +const NOW = 1_750_000_000_000; +const SNAPSHOT_KEY = 'glanceable-snapshot'; +const SCOPE_KEY = 'glanceable-scope-key'; + +const store = new Map(); + +// Fake SecureStore surface backed by an in-memory Map, injected through the +// test-only setter so the durable mirror never loads the real native module. +const secureStoreMock = { + setItemAsync: vi.fn(async (key: string, value: string) => { + store.set(key, value); + await Promise.resolve(); + }), + getItemAsync: vi.fn(async (key: string) => { + await Promise.resolve(); + return store.get(key) ?? null; + }), +}; + +function snapshotFor(sessions: { status: string }[]): GlanceableAgentsSnapshot { + return buildGlanceableSnapshot({ + sessions, + userId: 'u1', + organizationId: null, + now: NOW, + }); +} + +function deferred(): { promise: Promise; resolve: () => void } { + let storedResolve: (() => void) | undefined = undefined; + const promise = new Promise(resolve => { + storedResolve = resolve; + }); + return { + promise, + resolve: () => { + storedResolve?.(); + }, + }; +} + +beforeEach(() => { + _resetGlanceablePersistForTests(); + _setSecureStoreForTests(secureStoreMock); + store.clear(); + vi.clearAllMocks(); +}); + +afterEach(() => { + _resetGlanceablePersistForTests(); + store.clear(); +}); + +describe('restorePersistedGlanceable', () => { + it('does not clobber a snapshot written after the restore read started', async () => { + const stale = snapshotFor([]); + const staleRaw = JSON.stringify(stale); + store.set(SNAPSHOT_KEY, staleRaw); + store.set(SCOPE_KEY, 'stale-scope'); + + // Hold the restore read open so a live publish can land mid-read. + const gate = deferred(); + secureStoreMock.getItemAsync.mockImplementationOnce(async () => { + await gate.promise; + return staleRaw; + }); + + const restorePromise = restorePersistedGlanceable(); + + // A live publish lands while the persisted read is still pending. + const fresh = snapshotFor([{ status: 'busy' }]); + persistGlanceableSink.publish(fresh); + + gate.resolve(); + await restorePromise; + + expect(getLastGlanceableSnapshot()).toEqual(fresh); + expect(getLocalScopeKey()).toBe(fresh.scopeKey); + }); + + it('rejects a malformed stored record and keeps no snapshot', async () => { + store.set(SNAPSHOT_KEY, JSON.stringify({ schemaVersion: 1, revision: 'nope' })); + store.set(SCOPE_KEY, 'scope'); + + await restorePersistedGlanceable(); + + expect(getLastGlanceableSnapshot()).toBeNull(); + }); + + it('restores a schema-valid stored record', async () => { + const stored = snapshotFor([{ status: 'busy' }]); + store.set(SNAPSHOT_KEY, JSON.stringify(stored)); + store.set(SCOPE_KEY, stored.scopeKey); + + await restorePersistedGlanceable(); + + expect(getLastGlanceableSnapshot()).toEqual(stored); + expect(getLocalScopeKey()).toBe(stored.scopeKey); + }); +}); diff --git a/apps/mobile/src/lib/glanceable/persist.ts b/apps/mobile/src/lib/glanceable/persist.ts new file mode 100644 index 0000000000..5263448f54 --- /dev/null +++ b/apps/mobile/src/lib/glanceable/persist.ts @@ -0,0 +1,139 @@ +import { + type GlanceableAgentsSnapshot, + glanceableAgentsSnapshotSchema, +} from '@kilocode/app-shared/glanceable-agents-snapshot'; + +import { type GlanceableSink } from './sink-registry'; + +/** + * Durable mirror of the last glanceable snapshot and its scope key, for JS + * restart fencing only. The snapshot holds generic status, counts, timestamps + * and an opaque scope key — never titles, ids, or other raw content. + * + * iOS widgets read the snapshot through expo-widgets `updateSnapshot` / + * `updateTimeline`; Android widgets read through `react-native-android-widget` + * storage. This store exists so the background push handler can compare an + * incoming scope key without React context. + */ + +// SecureStore keys are defined here (not storage-keys.ts) so this module stays +// self-contained; nothing else owns these two keys. +const GLANCEABLE_SNAPSHOT_KEY = 'glanceable-snapshot'; +const GLANCEABLE_SCOPE_KEY = 'glanceable-scope-key'; + +type SecureStoreLike = { + setItemAsync: (key: string, value: string) => Promise; + getItemAsync: (key: string) => Promise; +}; + +// Test-only override so pure suites do not load expo-secure-store +// (→ expo-modules-core → RN). Mirrors the deep-link-launch pattern. +let secureStoreForTests: SecureStoreLike | null = null; + +function getSecureStore(): SecureStoreLike { + if (secureStoreForTests) { + return secureStoreForTests; + } + // eslint-disable-next-line typescript-eslint/no-require-imports, typescript-eslint/no-var-requires, unicorn/prefer-module -- lazy native load + return require('expo-secure-store') as SecureStoreLike; +} + +let lastSnapshot: GlanceableAgentsSnapshot | null = null; +let localScopeKey: string | null = null; + +// Monotonic epoch bumped on every in-memory write. Restore captures it before +// its async read and only fills when it is unchanged, so a live publish during +// the read can never be clobbered by a stale persisted record. +let persistEpoch = 0; + +export function getLastGlanceableSnapshot(): GlanceableAgentsSnapshot | null { + return lastSnapshot; +} + +export function getLocalScopeKey(): string | null { + return localScopeKey; +} + +/** In-memory write plus a fire-and-forget SecureStore mirror. */ +function persistSnapshot(snapshot: GlanceableAgentsSnapshot): void { + persistEpoch += 1; + lastSnapshot = snapshot; + localScopeKey = snapshot.scopeKey; + void getSecureStore().setItemAsync(GLANCEABLE_SNAPSHOT_KEY, JSON.stringify(snapshot)); + void getSecureStore().setItemAsync(GLANCEABLE_SCOPE_KEY, snapshot.scopeKey); +} + +/** Parse a stored record with the shared schema; a malformed record is absent. */ +function parseStoredSnapshot(raw: string): GlanceableAgentsSnapshot | null { + try { + const parsed: unknown = JSON.parse(raw); + const result = glanceableAgentsSnapshotSchema.safeParse(parsed); + return result.success ? result.data : null; + } catch { + return null; + } +} + +/** + * Restore the in-memory state from SecureStore after a JS restart. Best + * effort: a failed read keeps the null in-memory state. A live write during + * the read owns the state, so the stale persisted record is skipped. + */ +export async function restorePersistedGlanceable(): Promise { + const startEpoch = persistEpoch; + try { + const [rawSnapshot, rawScope] = await Promise.all([ + getSecureStore().getItemAsync(GLANCEABLE_SNAPSHOT_KEY), + getSecureStore().getItemAsync(GLANCEABLE_SCOPE_KEY), + ]); + // A live write landed during the read: it owns the state; skip the fill. + if (persistEpoch !== startEpoch) { + return; + } + if (rawSnapshot !== null) { + const parsed = parseStoredSnapshot(rawSnapshot); + if (parsed !== null) { + lastSnapshot = parsed; + } + } + if (rawScope !== null) { + localScopeKey = rawScope; + } + } catch { + // A malformed mirror is treated as absent; the publisher repopulates it. + } +} + +/** The persist sink owns no native surface, so endImmediate is a no-op. */ +export const persistGlanceableSink: GlanceableSink = { + publish(snapshot) { + persistSnapshot(snapshot); + }, + endImmediate() { + // The widget snapshot stays for later reads; nothing to end. + }, + startOrUpdate(snapshot) { + persistSnapshot(snapshot); + }, +}; + +// ── Test-only helpers ────────────────────────────────────────────────────── + +export function _setSecureStoreForTests(store: SecureStoreLike | null): void { + secureStoreForTests = store; +} + +export function _setLastGlanceableSnapshotForTests( + snapshot: GlanceableAgentsSnapshot | null +): void { + persistEpoch += 1; + lastSnapshot = snapshot; + localScopeKey = snapshot?.scopeKey ?? null; +} + +export function _resetGlanceablePersistForTests(): void { + persistEpoch = 0; + lastSnapshot = null; + localScopeKey = null; + secureStoreForTests = null; +} diff --git a/apps/mobile/src/lib/glanceable/presentation.test.ts b/apps/mobile/src/lib/glanceable/presentation.test.ts new file mode 100644 index 0000000000..87177b1294 --- /dev/null +++ b/apps/mobile/src/lib/glanceable/presentation.test.ts @@ -0,0 +1,108 @@ +import { describe, expect, it } from 'vitest'; + +import { + buildGlanceableSnapshot, + type GlanceableAgentsSnapshot, +} from '@kilocode/app-shared/glanceable-agents-snapshot'; + +import { + glanceableSpokenLabelKeys, + glanceableStatusCopyKey, + primaryGlanceableCount, + resolveGlanceableStatus, +} from './presentation'; + +const NOW = 1_750_000_000_000; + +function snapshot(overrides: { + sessions?: { status: string }[]; + status?: GlanceableAgentsSnapshot['status']; +}): GlanceableAgentsSnapshot { + return buildGlanceableSnapshot({ + sessions: overrides.sessions ?? [], + userId: 'u1', + organizationId: null, + now: NOW, + status: overrides.status, + }); +} + +describe('presentation precedence', () => { + it('signed-out flag wins over org-invalid and the snapshot status', () => { + expect(resolveGlanceableStatus(snapshot({ status: 'happy' }), { signedOut: true })).toBe( + 'signed_out' + ); + expect( + resolveGlanceableStatus(snapshot({ status: 'happy' }), { signedOut: true, orgInvalid: true }) + ).toBe('signed_out'); + }); + + it('org-invalid flag maps to privacy over the snapshot status', () => { + expect(resolveGlanceableStatus(snapshot({ status: 'stale' }), { orgInvalid: true })).toBe( + 'privacy' + ); + }); + + it('falls back to the snapshot status when no flag is set', () => { + expect(resolveGlanceableStatus(snapshot({ status: 'waiting' }))).toBe('waiting'); + expect(resolveGlanceableStatus(snapshot({ status: 'happy' }))).toBe('happy'); + }); +}); + +describe('primary rank and locked copy keys', () => { + it('ranks needs-input, then reconnecting, then running', () => { + const mixed = snapshot({ + sessions: [ + { status: 'busy' }, + { status: 'busy' }, + { status: 'busy' }, + { status: 'retry' }, + { status: 'question' }, + ], + }); + expect(primaryGlanceableCount(mixed)).toEqual({ key: 'glanceable.needsInput', count: 1 }); + + const noInput = snapshot({ sessions: [{ status: 'busy' }, { status: 'retry' }] }); + expect(primaryGlanceableCount(noInput)).toEqual({ key: 'glanceable.reconnecting', count: 1 }); + + const onlyRunning = snapshot({ sessions: [{ status: 'busy' }, { status: 'busy' }] }); + expect(primaryGlanceableCount(onlyRunning)).toEqual({ key: 'glanceable.running', count: 2 }); + + expect(primaryGlanceableCount(snapshot({}))).toBeNull(); + }); + + it('maps each non-happy status to its locked copy key', () => { + expect(glanceableStatusCopyKey(snapshot({ status: 'waiting' }))).toBe('glanceable.waiting'); + expect(glanceableStatusCopyKey(snapshot({ status: 'empty' }))).toBe('glanceable.empty'); + expect(glanceableStatusCopyKey(snapshot({ status: 'stale' }))).toBe('glanceable.stale'); + expect(glanceableStatusCopyKey(snapshot({ status: 'expired' }))).toBe('glanceable.expired'); + expect(glanceableStatusCopyKey(snapshot({ status: 'signed_out' }))).toBe( + 'glanceable.signedOut' + ); + expect(glanceableStatusCopyKey(snapshot({ status: 'privacy' }))).toBe('glanceable.privacy'); + expect(glanceableStatusCopyKey(snapshot({ status: 'happy' }))).toBeNull(); + }); +}); + +describe('spoken label shape', () => { + it('speaks counts then Open agents for happy, never a title or id', () => { + const happy = snapshot({ sessions: [{ status: 'busy' }, { status: 'question' }] }); + expect(glanceableSpokenLabelKeys(happy)).toEqual([ + 'glanceable.needsInput', + 'glanceable.running', + 'glanceable.openAgents', + ]); + expect(glanceableSpokenLabelKeys(happy).join(' ')).not.toContain('u1'); + }); + + it('speaks the status word then Open agents for non-happy statuses', () => { + expect(glanceableSpokenLabelKeys(snapshot({ status: 'empty' }))).toEqual([ + 'glanceable.empty', + 'glanceable.openAgents', + ]); + expect(glanceableSpokenLabelKeys(snapshot({ status: 'signed_out' }))).toEqual([ + 'glanceable.signedOut', + 'glanceable.openAgents', + ]); + }); +}); diff --git a/apps/mobile/src/lib/glanceable/presentation.ts b/apps/mobile/src/lib/glanceable/presentation.ts new file mode 100644 index 0000000000..5935d60d46 --- /dev/null +++ b/apps/mobile/src/lib/glanceable/presentation.ts @@ -0,0 +1,107 @@ +import { type GlanceableAgentsSnapshot } from '@kilocode/app-shared/glanceable-agents-snapshot'; + +/** + * Maps the snapshot plus optional surface flags to the locked copy keys, the + * compact primary count, and the spoken-label shape. Precedence: + * signed-out, then privacy/lost-org, then the snapshot's own status. + */ + +export type GlanceableStatus = GlanceableAgentsSnapshot['status']; + +/** Locked copy key per non-happy status. Happy shows counts, not a status line. */ +export const GLANCEABLE_STATUS_COPY_KEY = { + waiting: 'glanceable.waiting', + empty: 'glanceable.empty', + stale: 'glanceable.stale', + expired: 'glanceable.expired', + signed_out: 'glanceable.signedOut', + privacy: 'glanceable.privacy', +} as const satisfies Record, string>; + +export type GlanceableCountKey = + | 'glanceable.running' + | 'glanceable.needsInput' + | 'glanceable.reconnecting'; + +export type GlanceableCountLine = { key: GlanceableCountKey; count: number }; + +/** Rank order: needs-input, then reconnecting, then running. */ +const COUNT_ORDER: readonly { + key: GlanceableCountKey; + field: 'running' | 'needsInput' | 'reconnecting'; +}[] = [ + { key: 'glanceable.needsInput', field: 'needsInput' }, + { key: 'glanceable.reconnecting', field: 'reconnecting' }, + { key: 'glanceable.running', field: 'running' }, +]; + +/** Every non-zero count in rank order (expanded, medium, large, spoken). */ +export function glanceableCountLines(snapshot: GlanceableAgentsSnapshot): GlanceableCountLine[] { + const lines: GlanceableCountLine[] = []; + for (const { key, field } of COUNT_ORDER) { + const count = snapshot[field]; + if (count > 0) { + lines.push({ key, count }); + } + } + return lines; +} + +/** The single ranked count for compact surfaces; null when nothing is eligible. */ +export function primaryGlanceableCount( + snapshot: GlanceableAgentsSnapshot +): GlanceableCountLine | null { + return glanceableCountLines(snapshot)[0] ?? null; +} + +export type GlanceableSurfaceFlags = { + /** Forced by the auth context when signed out. */ + signedOut?: boolean; + /** Forced by the org fence when the selected org is no longer in the list. */ + orgInvalid?: boolean; +}; + +/** Resolve the display status under the surface precedence list. */ +export function resolveGlanceableStatus( + snapshot: GlanceableAgentsSnapshot, + flags: GlanceableSurfaceFlags = {} +): GlanceableStatus { + if (flags.signedOut) { + return 'signed_out'; + } + if (flags.orgInvalid) { + return 'privacy'; + } + return snapshot.status; +} + +/** The top-line copy key for the snapshot, or null for happy (counts only). */ +export function glanceableStatusCopyKey( + snapshot: GlanceableAgentsSnapshot, + flags: GlanceableSurfaceFlags = {} +): string | null { + const status = resolveGlanceableStatus(snapshot, flags); + return status === 'happy' ? null : GLANCEABLE_STATUS_COPY_KEY[status]; +} + +/** + * Ordered spoken-label parts: status words, counts, then Open agents. Never a + * title, organization name, or id. Each part is a copy key the surface + * resolves to its translated string. + */ +export function glanceableSpokenLabelKeys( + snapshot: GlanceableAgentsSnapshot, + flags: GlanceableSurfaceFlags = {} +): string[] { + const status = resolveGlanceableStatus(snapshot, flags); + const parts: string[] = []; + if (status === 'happy' || status === 'stale') { + for (const { key } of glanceableCountLines(snapshot)) { + parts.push(key); + } + } else { + parts.push(GLANCEABLE_STATUS_COPY_KEY[status]); + } + parts.push('glanceable.openAgents'); + return parts; +} diff --git a/apps/mobile/src/lib/glanceable/publisher.test.ts b/apps/mobile/src/lib/glanceable/publisher.test.ts new file mode 100644 index 0000000000..a470bdf8c9 --- /dev/null +++ b/apps/mobile/src/lib/glanceable/publisher.test.ts @@ -0,0 +1,243 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { + buildGlanceableSnapshot, + GLANCEABLE_SNAPSHOT_EXPIRY_MS, + type GlanceableAgentsSnapshot, +} from '@kilocode/app-shared/glanceable-agents-snapshot'; + +import { getTerminalBlankEpoch, writeSignedOutSnapshotAndEnd } from './cleanup'; +import { GlanceablePublisher } from './publisher'; +import { + type GlanceableSink, + type GlanceableSinkContext, + registerGlanceableSink, + unregisterGlanceableSink, +} from './sink-registry'; + +const NOW = 1_750_000_000_000; +const PUB_CTX = { userId: 'u1', organizationId: null }; + +type SinkCall = + | { type: 'publish'; snapshot: GlanceableAgentsSnapshot } + | { type: 'startOrUpdate'; snapshot: GlanceableAgentsSnapshot; ctx: GlanceableSinkContext } + | { type: 'endImmediate' }; + +function makeSink() { + const calls: SinkCall[] = []; + const sink: GlanceableSink = { + publish(snapshot) { + calls.push({ type: 'publish', snapshot }); + }, + startOrUpdate(snapshot, ctx) { + calls.push({ type: 'startOrUpdate', snapshot, ctx }); + }, + endImmediate() { + calls.push({ type: 'endImmediate' }); + }, + }; + return { sink, calls }; +} + +function count(calls: SinkCall[], type: SinkCall['type']): number { + return calls.filter(call => call.type === type).length; +} + +function lastSnapshot( + calls: SinkCall[], + type: 'publish' | 'startOrUpdate' +): GlanceableAgentsSnapshot { + const found = [...calls].toReversed().find(call => call.type === type); + if (found === undefined) { + throw new Error(`no ${type} call`); + } + return (found as { snapshot: GlanceableAgentsSnapshot }).snapshot; +} + +function snapshotFor(sessions: { status: string }[], now: number, revision = 0) { + return buildGlanceableSnapshot({ + sessions, + userId: 'u1', + organizationId: null, + now, + previousRevision: revision, + }); +} + +afterEach(() => { + vi.useRealTimers(); +}); + +describe('GlanceablePublisher', () => { + it('derives the count map from the session rows', () => { + const { sink, calls } = makeSink(); + const publisher = new GlanceablePublisher({ sinks: [sink], now: () => NOW }); + publisher.handleSessions( + [ + { status: 'busy' }, + { status: 'busy' }, + { status: 'question' }, + { status: 'retry' }, + { status: 'idle' }, + ], + PUB_CTX + ); + const snapshot = lastSnapshot(calls, 'startOrUpdate'); + expect(snapshot.running).toBe(2); + expect(snapshot.needsInput).toBe(1); + expect(snapshot.reconnecting).toBe(1); + expect(snapshot.status).toBe('happy'); + }); + + it('starts the activity immediately on the first eligible emit', () => { + vi.useFakeTimers(); + const { sink, calls } = makeSink(); + const publisher = new GlanceablePublisher({ sinks: [sink], now: () => NOW }); + publisher.handleSessions([{ status: 'busy' }], PUB_CTX); + expect(count(calls, 'startOrUpdate')).toBe(1); + publisher.dispose(); + }); + + it('coalesces later happy updates and emits only the latest', () => { + vi.useFakeTimers(); + const { sink, calls } = makeSink(); + const publisher = new GlanceablePublisher({ sinks: [sink], now: () => NOW, coalesceMs: 1000 }); + publisher.handleSessions([{ status: 'busy' }], PUB_CTX); + publisher.handleSessions([{ status: 'busy' }, { status: 'busy' }], PUB_CTX); + publisher.handleSessions([{ status: 'busy' }, { status: 'busy' }, { status: 'busy' }], PUB_CTX); + expect(count(calls, 'startOrUpdate')).toBe(1); + vi.advanceTimersByTime(1000); + expect(count(calls, 'startOrUpdate')).toBe(2); + expect(lastSnapshot(calls, 'startOrUpdate').running).toBe(3); + publisher.dispose(); + }); + + it('discards an incoming older revision', () => { + const { sink, calls } = makeSink(); + const publisher = new GlanceablePublisher({ sinks: [sink], now: () => NOW }); + publisher.applySnapshot(snapshotFor([{ status: 'busy' }], NOW, 4), PUB_CTX); + const started = count(calls, 'startOrUpdate'); + publisher.applySnapshot(snapshotFor([{ status: 'busy' }], NOW, 2), PUB_CTX); + expect(count(calls, 'startOrUpdate')).toBe(started); + }); + + it('publishes empty for idle-only sessions without starting or ending', () => { + vi.useFakeTimers(); + const { sink, calls } = makeSink(); + const publisher = new GlanceablePublisher({ sinks: [sink], now: () => NOW }); + publisher.handleSessions([{ status: 'idle' }, { status: 'idle' }], PUB_CTX); + expect(count(calls, 'startOrUpdate')).toBe(0); + expect(lastSnapshot(calls, 'publish').status).toBe('empty'); + vi.advanceTimersByTime(8000); + expect(count(calls, 'endImmediate')).toBe(0); + publisher.dispose(); + }); + + it('distinguishes waiting (first fetch) from empty (fetch settled)', () => { + const { sink, calls } = makeSink(); + const publisher = new GlanceablePublisher({ sinks: [sink], now: () => NOW }); + publisher.handleFetchStarted(PUB_CTX); + expect(lastSnapshot(calls, 'publish').status).toBe('waiting'); + expect(count(calls, 'startOrUpdate')).toBe(0); + publisher.handleSessions([{ status: 'idle' }], PUB_CTX); + expect(lastSnapshot(calls, 'publish').status).toBe('empty'); + }); + + it('keeps counts on stale and hides counts on expired', () => { + const { sink, calls } = makeSink(); + const publisher = new GlanceablePublisher({ sinks: [sink], now: () => NOW }); + publisher.handleSessions([{ status: 'busy' }], PUB_CTX); + publisher.handleFetchError(PUB_CTX); + expect(lastSnapshot(calls, 'publish').status).toBe('stale'); + expect(lastSnapshot(calls, 'publish').running).toBe(1); + + let now = NOW; + const { sink: sink2, calls: calls2 } = makeSink(); + const publisher2 = new GlanceablePublisher({ sinks: [sink2], now: () => now }); + publisher2.handleSessions([{ status: 'busy' }], PUB_CTX); + now = NOW + GLANCEABLE_SNAPSHOT_EXPIRY_MS; + publisher2.handleSessions([{ status: 'busy' }], PUB_CTX); + const expired = calls2.filter( + (call): call is { type: 'publish'; snapshot: GlanceableAgentsSnapshot } => + call.type === 'publish' && call.snapshot.status === 'expired' + ); + expect(expired.length).toBe(1); + expect(expired[0]?.snapshot.running).toBe(0); + }); + + it('does not schedule the 8s terminal for a signed-out snapshot', () => { + vi.useFakeTimers(); + const { sink, calls } = makeSink(); + const publisher = new GlanceablePublisher({ sinks: [sink], now: () => NOW }); + publisher.applySnapshot( + buildGlanceableSnapshot({ + sessions: [], + userId: 'u1', + organizationId: null, + now: NOW, + status: 'signed_out', + }), + PUB_CTX + ); + vi.advanceTimersByTime(8000); + expect(count(calls, 'endImmediate')).toBe(0); + publisher.dispose(); + }); + + it('does not publish or restart after a terminal blank', () => { + const { sink, calls } = makeSink(); + registerGlanceableSink(sink); + const publisher = new GlanceablePublisher({ + sinks: [sink], + now: () => NOW, + terminalBlankEpoch: getTerminalBlankEpoch, + }); + try { + publisher.handleSessions([{ status: 'busy' }], PUB_CTX); + expect(count(calls, 'startOrUpdate')).toBe(1); + expect(count(calls, 'publish')).toBe(1); + + writeSignedOutSnapshotAndEnd(); + expect(lastSnapshot(calls, 'publish').status).toBe('signed_out'); + expect(count(calls, 'endImmediate')).toBe(1); + + // A live cache success after the blank must not publish or restart. + publisher.handleSessions([{ status: 'busy' }, { status: 'busy' }], PUB_CTX); + expect(count(calls, 'startOrUpdate')).toBe(1); + expect(count(calls, 'publish')).toBe(2); + expect(lastSnapshot(calls, 'publish').status).toBe('signed_out'); + } finally { + unregisterGlanceableSink(sink); + publisher.dispose(); + } + }); + + it('drops a pending coalesced emit after a terminal blank', () => { + vi.useFakeTimers(); + const { sink, calls } = makeSink(); + const publisher = new GlanceablePublisher({ + sinks: [sink], + now: () => NOW, + coalesceMs: 1000, + terminalBlankEpoch: getTerminalBlankEpoch, + }); + publisher.handleSessions([{ status: 'busy' }], PUB_CTX); + publisher.handleSessions([{ status: 'busy' }, { status: 'busy' }], PUB_CTX); + expect(count(calls, 'startOrUpdate')).toBe(1); + + writeSignedOutSnapshotAndEnd(); + vi.advanceTimersByTime(1000); + expect(count(calls, 'startOrUpdate')).toBe(1); + publisher.dispose(); + }); + + it('keeps the revision monotonic when seeded from an initial snapshot', () => { + const { sink, calls } = makeSink(); + // Seeded with revision 42; the next snapshot must be 43. + const initial = snapshotFor([{ status: 'busy' }], NOW - 60_000, 41); + const publisher = new GlanceablePublisher({ sinks: [sink], now: () => NOW, initial }); + publisher.handleSessions([{ status: 'busy' }], PUB_CTX); + expect(lastSnapshot(calls, 'startOrUpdate').revision).toBe(43); + publisher.dispose(); + }); +}); diff --git a/apps/mobile/src/lib/glanceable/publisher.ts b/apps/mobile/src/lib/glanceable/publisher.ts new file mode 100644 index 0000000000..af203542d7 --- /dev/null +++ b/apps/mobile/src/lib/glanceable/publisher.ts @@ -0,0 +1,260 @@ +import { + buildGlanceableSnapshot, + GLANCEABLE_COALESCE_MS, + GLANCEABLE_SNAPSHOT_EXPIRY_MS, + GLANCEABLE_TERMINAL_MS, + type GlanceableAgentsSnapshot, + type GlanceableAgentsSnapshotStatus, + isEligibleGlanceableWork, + shouldDiscardGlanceableRevision, +} from '@kilocode/app-shared/glanceable-agents-snapshot'; + +import { type GlanceableSink, type GlanceableSinkContext } from './sink-registry'; + +/** + * Framework-agnostic publisher state machine. Derives one versioned snapshot + * from the active-sessions tray cache, coalesces later happy updates, starts + * the activity on the first eligible emit, and schedules the 8 s terminal end + * when work becomes empty. The React glue is `mount.tsx`. + */ + +export type GlanceablePublisherContext = { + userId: string; + organizationId: string | null; +}; + +export type GlanceablePublisherOptions = { + sinks: readonly GlanceableSink[]; + /** Seeded from the persisted last snapshot so revision stays monotonic. */ + initial?: GlanceableAgentsSnapshot | null; + now?: () => number; + coalesceMs?: number; + terminalMs?: number; + /** + * Monotonic terminal-blank epoch reader (see cleanup). The publisher captures + * it at construction and refuses to emit once it advances, so a live cache + * success after a signed-out or privacy blank cannot republish or restart. + */ + terminalBlankEpoch?: () => number; +}; + +type TimerHandle = ReturnType; + +/** Copy a snapshot with a new status and a fresh revision/updatedAt/expiresAt. */ +export function withStatus( + snapshot: GlanceableAgentsSnapshot, + status: GlanceableAgentsSnapshotStatus, + now: number +): GlanceableAgentsSnapshot { + const updatedAt = new Date(now).toISOString(); + return { + ...snapshot, + revision: snapshot.revision + 1, + updatedAt, + expiresAt: new Date(now + GLANCEABLE_SNAPSHOT_EXPIRY_MS).toISOString(), + status, + }; +} + +export class GlanceablePublisher { + private readonly sinks: readonly GlanceableSink[]; + private readonly now: () => number; + private readonly coalesceMs: number; + private readonly terminalMs: number; + private readonly terminalBlankEpoch: () => number; + private readonly blankEpochAtStart: number; + private current: GlanceableAgentsSnapshot | null; + private activityStarted: boolean; + private coalesceTimer: TimerHandle | null = null; + private terminalTimer: TimerHandle | null = null; + private pendingCoalesced: { + snapshot: GlanceableAgentsSnapshot; + ctx: GlanceableSinkContext; + } | null = null; + + constructor(options: GlanceablePublisherOptions) { + this.sinks = options.sinks; + this.now = options.now ?? (() => Date.now()); + this.coalesceMs = options.coalesceMs ?? GLANCEABLE_COALESCE_MS; + this.terminalMs = options.terminalMs ?? GLANCEABLE_TERMINAL_MS; + this.terminalBlankEpoch = options.terminalBlankEpoch ?? (() => 0); + this.blankEpochAtStart = this.terminalBlankEpoch(); + this.current = options.initial ?? null; + this.activityStarted = false; + } + + /** Cache success: derive the next snapshot from the current session rows. */ + handleSessions(sessions: readonly { status: string }[], ctx: GlanceablePublisherContext): void { + if (this.isGated()) { + return; + } + const now = this.now(); + this.applyExpiry(now, ctx); + + const snapshot = buildGlanceableSnapshot({ + sessions, + userId: ctx.userId, + organizationId: ctx.organizationId, + now, + previousRevision: this.current?.revision ?? 0, + previousEligibleStartedAt: this.current?.eligibleStartedAt ?? null, + }); + + if (isEligibleGlanceableWork(snapshot)) { + this.cancelTerminal(); + if (!this.activityStarted) { + // First eligible emit starts the activity immediately, no coalesce wait. + this.emit(snapshot, ctx); + this.activityStarted = true; + } else { + this.scheduleCoalesced(snapshot, ctx); + } + } else { + this.cancelCoalesce(); + this.publish(snapshot); + if (this.activityStarted) { + // Happy → empty: terminal end after the brief empty window. + this.scheduleTerminal(); + } + this.activityStarted = false; + } + this.current = snapshot; + } + + /** First fetch in flight with no snapshot yet: waiting, never started. */ + handleFetchStarted(ctx: GlanceablePublisherContext): void { + if (this.isGated()) { + return; + } + if (this.current !== null) { + return; + } + const snapshot = buildGlanceableSnapshot({ + sessions: [], + userId: ctx.userId, + organizationId: ctx.organizationId, + now: this.now(), + status: 'waiting', + }); + this.publish(snapshot); + this.current = snapshot; + } + + /** Cache update failed: republish the last counts with a stale status. */ + handleFetchError(ctx: GlanceablePublisherContext): void { + if (this.isGated()) { + return; + } + const now = this.now(); + if (this.applyExpiry(now, ctx) || this.current === null) { + return; + } + const snapshot = withStatus(this.current, 'stale', now); + this.publish(snapshot); + this.current = snapshot; + } + + /** + * Apply an incoming snapshot (future background delivery). Older revisions + * are discarded; the local account epoch is applied by the caller. + */ + applySnapshot(incoming: GlanceableAgentsSnapshot, ctx: GlanceablePublisherContext): void { + if (this.current !== null && shouldDiscardGlanceableRevision(incoming, this.current)) { + return; + } + if (isEligibleGlanceableWork(incoming)) { + this.emit(incoming, ctx); + this.activityStarted = true; + } else { + this.publish(incoming); + this.activityStarted = false; + } + this.current = incoming; + } + + dispose(): void { + this.cancelCoalesce(); + this.cancelTerminal(); + } + + private isGated(): boolean { + return this.terminalBlankEpoch() !== this.blankEpochAtStart; + } + + private emit(snapshot: GlanceableAgentsSnapshot, ctx: GlanceableSinkContext): void { + for (const sink of this.sinks) { + sink.publish(snapshot); + sink.startOrUpdate(snapshot, ctx); + } + } + + private publish(snapshot: GlanceableAgentsSnapshot): void { + for (const sink of this.sinks) { + sink.publish(snapshot); + } + } + + private scheduleCoalesced(snapshot: GlanceableAgentsSnapshot, ctx: GlanceableSinkContext): void { + this.pendingCoalesced = { snapshot, ctx }; + if (this.coalesceTimer !== null) { + return; + } + this.coalesceTimer = setTimeout(() => { + this.coalesceTimer = null; + const pending = this.pendingCoalesced; + this.pendingCoalesced = null; + if (pending !== null && !this.isGated()) { + this.emit(pending.snapshot, pending.ctx); + } + }, this.coalesceMs); + } + + private scheduleTerminal(): void { + if (this.terminalTimer !== null) { + return; + } + this.terminalTimer = setTimeout(() => { + this.terminalTimer = null; + this.activityStarted = false; + for (const sink of this.sinks) { + sink.endImmediate(); + } + }, this.terminalMs); + } + + private cancelCoalesce(): void { + if (this.coalesceTimer !== null) { + clearTimeout(this.coalesceTimer); + this.coalesceTimer = null; + } + this.pendingCoalesced = null; + } + + private cancelTerminal(): void { + if (this.terminalTimer !== null) { + clearTimeout(this.terminalTimer); + this.terminalTimer = null; + } + } + + /** Publish an expired snapshot (zero counts) once the current one lapses. */ + private applyExpiry(now: number, ctx: GlanceablePublisherContext): boolean { + if (this.current === null || now < Date.parse(this.current.expiresAt)) { + return false; + } + const snapshot = buildGlanceableSnapshot({ + sessions: [], + userId: ctx.userId, + organizationId: ctx.organizationId, + now, + previousRevision: this.current.revision, + status: 'expired', + }); + this.cancelCoalesce(); + this.cancelTerminal(); + this.publish(snapshot); + this.current = snapshot; + this.activityStarted = false; + return true; + } +} diff --git a/apps/mobile/src/lib/glanceable/sink-registry.ts b/apps/mobile/src/lib/glanceable/sink-registry.ts new file mode 100644 index 0000000000..30228c3dbe --- /dev/null +++ b/apps/mobile/src/lib/glanceable/sink-registry.ts @@ -0,0 +1,57 @@ +import { type GlanceableAgentsSnapshot } from '@kilocode/app-shared/glanceable-agents-snapshot'; + +/** + * One sink consumes the glanceable snapshot for one native surface (persist, + * iOS Live Activity/widgets, Android widget/ongoing). Platform sinks register + * themselves from files their slices own; the layout mount registers only the + * persist sink. + */ +export type GlanceableSinkContext = { + /** For token registration only; must never enter the snapshot. */ + organizationId: string | null; +}; + +export type GlanceableSink = { + publish(snapshot: GlanceableAgentsSnapshot): void; + endImmediate(): void; + startOrUpdate(snapshot: GlanceableAgentsSnapshot, ctx: GlanceableSinkContext): void; +}; + +const sinks = new Set(); + +export function registerGlanceableSink(sink: GlanceableSink): void { + sinks.add(sink); +} + +export function unregisterGlanceableSink(sink: GlanceableSink): void { + sinks.delete(sink); +} + +export function getGlanceableSinks(): readonly GlanceableSink[] { + return [...sinks]; +} + +/** Activity-token registrar, set by a later token slice. No-op by default. */ +export type GlanceableDelivery = { + registerTokens(snapshot: GlanceableAgentsSnapshot, organizationId: string | null): void; + unregisterTokens(): void; +}; + +const noopDelivery: GlanceableDelivery = { + registerTokens() { + // No-op until a token slice registers a delivery. + }, + unregisterTokens() { + // No-op until a token slice registers a delivery. + }, +}; + +let delivery: GlanceableDelivery = noopDelivery; + +export function setGlanceableDelivery(next: GlanceableDelivery): void { + delivery = next; +} + +export function getGlanceableDelivery(): GlanceableDelivery { + return delivery; +} diff --git a/apps/mobile/src/lib/organization-context.tsx b/apps/mobile/src/lib/organization-context.tsx index 1d64585776..2e485bb317 100644 --- a/apps/mobile/src/lib/organization-context.tsx +++ b/apps/mobile/src/lib/organization-context.tsx @@ -11,6 +11,7 @@ import { import { useAuth } from '@/lib/auth/auth-context'; import { deleteAccountMetadata, setAccountMetadata } from '@/lib/auth/account-metadata-write'; +import { writePrivacySnapshotAndEnd } from '@/lib/glanceable/cleanup'; import { ORGANIZATION_STORAGE_KEY } from '@/lib/storage-keys'; type OrganizationContextValue = { @@ -58,14 +59,26 @@ export function OrganizationProvider({ children }: { readonly children: ReactNod }; }, [token]); - const setOrganizationId = useCallback((id: string | null) => { - setOrgState(id); - if (id) { - void setAccountMetadata(ORGANIZATION_STORAGE_KEY, id); - } else { - void deleteAccountMetadata(ORGANIZATION_STORAGE_KEY); - } - }, []); + const setOrganizationId = useCallback( + (id: string | null) => { + // A same-value selection is a no-op: blanking it would bump the terminal + // epoch and permanently gate the publisher, because React bails out of the + // state update and no effect re-runs to rebuild it. + if (id === organizationId) { + return; + } + // Blank the current surface before the selection changes so the prior + // org's counts are never shown under the next org. + writePrivacySnapshotAndEnd(); + setOrgState(id); + if (id) { + void setAccountMetadata(ORGANIZATION_STORAGE_KEY, id); + } else { + void deleteAccountMetadata(ORGANIZATION_STORAGE_KEY); + } + }, + [organizationId] + ); const value = useMemo( () => ({ organizationId, isLoaded, setOrganizationId }), diff --git a/apps/mobile/vitest.pure.config.ts b/apps/mobile/vitest.pure.config.ts index 9ee4f91ea4..45c0e244d6 100644 --- a/apps/mobile/vitest.pure.config.ts +++ b/apps/mobile/vitest.pure.config.ts @@ -27,6 +27,7 @@ export default defineProject({ 'src/lib/auth/**/*.test.tsx', 'src/lib/apple-iap/**/*.test.ts', 'src/lib/apple-iap/**/*.test.tsx', + 'src/lib/glanceable/**/*.test.ts', 'src/lib/hooks/**/*.test.ts', 'src/lib/kilo-pass/**/*.test.ts', 'src/lib/kilo-pass/**/*.test.tsx', diff --git a/packages/app-shared/package.json b/packages/app-shared/package.json index d6d2f8f4bc..62f58c7aec 100644 --- a/packages/app-shared/package.json +++ b/packages/app-shared/package.json @@ -19,7 +19,8 @@ "./app-version": "./src/app-version.ts", "./pr-review": "./src/pr-review/index.ts", "./commerce": "./src/commerce/index.ts", - "./moderation": "./src/moderation/index.ts" + "./moderation": "./src/moderation/index.ts", + "./glanceable-agents-snapshot": "./src/glanceable-agents-snapshot.ts" }, "scripts": { "typecheck": "tsgo --noEmit", diff --git a/packages/app-shared/src/glanceable-agents-snapshot.test.ts b/packages/app-shared/src/glanceable-agents-snapshot.test.ts new file mode 100644 index 0000000000..c46f20b5c2 --- /dev/null +++ b/packages/app-shared/src/glanceable-agents-snapshot.test.ts @@ -0,0 +1,210 @@ +import { describe, expect, it } from 'vitest'; + +import { + buildGlanceableSnapshot, + buildOpaqueScopeKey, + countGlanceableSessions, + GLANCEABLE_SNAPSHOT_EXPIRY_MS, + isEligibleGlanceableWork, + shouldDiscardGlanceableRevision, +} from './glanceable-agents-snapshot'; + +const NOW = 1_750_000_000_000; + +describe('countGlanceableSessions', () => { + it('maps busy/question/permission/retry and ignores idle and unknown', () => { + const counts = countGlanceableSessions([ + { status: 'busy' }, + { status: 'busy' }, + { status: 'question' }, + { status: 'permission' }, + { status: 'permission' }, + { status: 'retry' }, + { status: 'idle' }, + { status: 'idle' }, + { status: 'completed' }, + { status: 'failed' }, + { status: 'mystery' }, + ]); + expect(counts).toEqual({ running: 2, needsInput: 3, reconnecting: 1 }); + }); + + it('counts Cloud Agent-shaped and CLI-shaped rows together on status alone', () => { + const cloudRow = { status: 'busy', kind: 'cloud-agent' }; + const cliRow = { status: 'retry', connectionId: 'cli-1' }; + expect(countGlanceableSessions([cloudRow, cliRow])).toEqual({ + running: 1, + needsInput: 0, + reconnecting: 1, + }); + }); + + it('produces zero eligible counts for idle-only sessions', () => { + expect(countGlanceableSessions([{ status: 'idle' }, { status: 'idle' }])).toEqual({ + running: 0, + needsInput: 0, + reconnecting: 0, + }); + }); +}); + +describe('buildOpaqueScopeKey', () => { + it('is stable for the same input and never returns the raw ids', () => { + const a = buildOpaqueScopeKey({ userId: 'oauth/user-1', organizationId: 'org-9' }); + const b = buildOpaqueScopeKey({ userId: 'oauth/user-1', organizationId: 'org-9' }); + expect(a).toBe(b); + expect(a).toMatch(/^[0-9a-f]{8}$/); + expect(a).not.toContain('oauth/user-1'); + expect(a).not.toContain('org-9'); + }); + + it('differs when the user or organization differs', () => { + const personal = buildOpaqueScopeKey({ userId: 'u1', organizationId: null }); + const org = buildOpaqueScopeKey({ userId: 'u1', organizationId: 'org-1' }); + const otherUser = buildOpaqueScopeKey({ userId: 'u2', organizationId: 'org-1' }); + expect(new Set([personal, org, otherUser]).size).toBe(3); + }); +}); + +describe('buildGlanceableSnapshot', () => { + it('increases revision and marks expiry 8 hours after updatedAt', () => { + const snapshot = buildGlanceableSnapshot({ + sessions: [{ status: 'busy' }], + userId: 'oauth/user-1', + organizationId: 'org-9', + now: NOW, + }); + expect(snapshot.revision).toBe(1); + expect(snapshot.status).toBe('happy'); + expect(snapshot.running).toBe(1); + expect(Date.parse(snapshot.expiresAt) - Date.parse(snapshot.updatedAt)).toBe( + GLANCEABLE_SNAPSHOT_EXPIRY_MS + ); + }); + + it('keeps revision monotonic and eligibleStartedAt while work stays eligible', () => { + const first = buildGlanceableSnapshot({ + sessions: [{ status: 'busy' }], + userId: 'u1', + organizationId: null, + now: NOW, + }); + const second = buildGlanceableSnapshot({ + sessions: [{ status: 'busy' }, { status: 'question' }], + userId: 'u1', + organizationId: null, + now: NOW + 5000, + previousRevision: first.revision, + previousEligibleStartedAt: first.eligibleStartedAt, + }); + expect(second.revision).toBe(first.revision + 1); + expect(second.eligibleStartedAt).toBe(first.eligibleStartedAt); + expect(second.needsInput).toBe(1); + }); + + it('clears eligibleStartedAt when no eligible work remains', () => { + const snapshot = buildGlanceableSnapshot({ + sessions: [{ status: 'idle' }], + userId: 'u1', + organizationId: null, + now: NOW, + previousRevision: 3, + previousEligibleStartedAt: new Date(NOW - 60_000).toISOString(), + }); + expect(snapshot.status).toBe('empty'); + expect(snapshot.eligibleStartedAt).toBeNull(); + expect(snapshot.revision).toBe(4); + }); + + it('sets organizationBound only when organizationId is a string', () => { + const personal = buildGlanceableSnapshot({ + sessions: [], + userId: 'u1', + organizationId: null, + now: NOW, + }); + const org = buildGlanceableSnapshot({ + sessions: [], + userId: 'u1', + organizationId: 'org-9', + now: NOW, + }); + expect(personal.organizationBound).toBe(false); + expect(org.organizationBound).toBe(true); + }); + + it('honours a status override and omits accountEpoch when absent', () => { + const snapshot = buildGlanceableSnapshot({ + sessions: [], + userId: 'u1', + organizationId: null, + now: NOW, + status: 'waiting', + }); + expect(snapshot.status).toBe('waiting'); + expect('accountEpoch' in snapshot).toBe(false); + }); + + it('serializes without any forbidden fixture', () => { + const rows = [ + { status: 'busy', title: 'Secret prompt', gitUrl: 'github.com/acme/repo', id: 'ses_raw_1' }, + { status: 'question', organizationName: 'Acme Org' }, + ]; + const snapshot = buildGlanceableSnapshot({ + sessions: rows, + userId: 'oauth/user-1', + organizationId: 'org-9', + now: NOW, + }); + const json = JSON.stringify(snapshot); + expect(json).not.toContain('Secret prompt'); + expect(json).not.toContain('Acme Org'); + expect(json).not.toContain('github.com/acme/repo'); + expect(json).not.toContain('ses_raw_1'); + expect(json).not.toContain('oauth/user-1'); + expect(json).not.toContain('org-9'); + }); +}); + +describe('isEligibleGlanceableWork and revision discard', () => { + it('reports eligibility from the three counts', () => { + const empty = buildGlanceableSnapshot({ sessions: [], userId: 'u1', organizationId: null, now: NOW }); + const busy = buildGlanceableSnapshot({ sessions: [{ status: 'busy' }], userId: 'u1', organizationId: null, now: NOW }); + expect(isEligibleGlanceableWork(empty)).toBe(false); + expect(isEligibleGlanceableWork(busy)).toBe(true); + }); + + it('discards a lower revision and an older updatedAt at equal revision', () => { + const current = buildGlanceableSnapshot({ + sessions: [{ status: 'busy' }], + userId: 'u1', + organizationId: null, + now: NOW, + previousRevision: 4, + }); + const lowerRevision = buildGlanceableSnapshot({ + sessions: [], + userId: 'u1', + organizationId: null, + now: NOW + 10_000, + previousRevision: 3, + }); + const olderAtEqualRevision = buildGlanceableSnapshot({ + sessions: [], + userId: 'u1', + organizationId: null, + now: NOW - 5000, + previousRevision: 4, + }); + const newerAtEqualRevision = buildGlanceableSnapshot({ + sessions: [], + userId: 'u1', + organizationId: null, + now: NOW + 15_000, + previousRevision: 4, + }); + expect(shouldDiscardGlanceableRevision(lowerRevision, current)).toBe(true); + expect(shouldDiscardGlanceableRevision(olderAtEqualRevision, current)).toBe(true); + expect(shouldDiscardGlanceableRevision(newerAtEqualRevision, current)).toBe(false); + }); +}); diff --git a/packages/app-shared/src/glanceable-agents-snapshot.ts b/packages/app-shared/src/glanceable-agents-snapshot.ts new file mode 100644 index 0000000000..4a492831cd --- /dev/null +++ b/packages/app-shared/src/glanceable-agents-snapshot.ts @@ -0,0 +1,187 @@ +import { z } from 'zod'; + +/** + * One durable, privacy-minimal, versioned snapshot for every glanceable + * surface (Live Activity, Dynamic Island, Home Screen widget, Android + * widget, Android ongoing notification). + * + * Privacy contract: the snapshot carries generic status, counts, safe + * timestamps, and an opaque scope key only. It must never carry a session + * title, prompt, excerpt, organization name, repository name, generated + * text, secret, or a raw account/session id. + */ + +export const GLANCEABLE_SNAPSHOT_SCHEMA_VERSION = 1; +/** 8 hours: matches the usual Live Activity lifetime. */ +export const GLANCEABLE_SNAPSHOT_EXPIRY_MS = 28_800_000; +/** Later happy updates are coalesced for at most this long. */ +export const GLANCEABLE_COALESCE_MS = 1000; +/** Terminal empty lasts at most this long before the activity ends. */ +export const GLANCEABLE_TERMINAL_MS = 8000; + +export type GlanceableAgentsSnapshotStatus = + | 'waiting' + | 'empty' + | 'happy' + | 'stale' + | 'expired' + | 'signed_out' + | 'privacy'; + +export const glanceableAgentsSnapshotSchema = z.object({ + schemaVersion: z.literal(1), + revision: z.number().int().min(1), + /** ISO 8601 timestamp. */ + updatedAt: z.string(), + /** ISO 8601 timestamp; `updatedAt + GLANCEABLE_SNAPSHOT_EXPIRY_MS`. */ + expiresAt: z.string(), + /** Opaque scope key; never a raw user or organization id. */ + scopeKey: z.string().min(1), + /** + * Client-set local auth epoch. Optional on the wire: old and server + * producers omit it; the client sets the current local epoch when it + * applies a remote snapshot. Remove the optional when every producer + * sends it. + */ + accountEpoch: z.number().int().optional(), + organizationBound: z.boolean(), + status: z.enum(['waiting', 'empty', 'happy', 'stale', 'expired', 'signed_out', 'privacy']), + running: z.number().int().min(0), + needsInput: z.number().int().min(0), + reconnecting: z.number().int().min(0), + /** ISO 8601 timestamp or null; binds the elapsed-time display. */ + eligibleStartedAt: z.string().nullable(), +}); + +export type GlanceableAgentsSnapshot = z.infer; + +export type GlanceableCounts = { + running: number; + needsInput: number; + reconnecting: number; +}; + +/** + * Map session rows to the three eligible counts. `busy` → running, + * `question`/`permission` → needs-input, `retry` → reconnecting. `idle` and + * any unknown status are ignored. Do not call `isCompletedStatus` here. + */ +export function countGlanceableSessions( + sessions: readonly { status: string }[] +): GlanceableCounts { + let running = 0; + let needsInput = 0; + let reconnecting = 0; + for (const session of sessions) { + switch (session.status) { + case 'busy': + running += 1; + break; + case 'question': + case 'permission': + needsInput += 1; + break; + case 'retry': + reconnecting += 1; + break; + default: + // idle and unknown statuses contribute nothing. + break; + } + } + return { running, needsInput, reconnecting }; +} + +// FNV-1a 32-bit over UTF-16 code units (two bytes each). Deterministic across +// Node and Hermes and not reversible to the input, so the raw ids never appear +// in the key. +function fnv1a32(input: string): number { + let hash = 0x811c9dc5; + for (let i = 0; i < input.length; i += 1) { + const code = input.charCodeAt(i); + hash = Math.imul(hash ^ (code & 0xff), 0x01000193) >>> 0; + hash = Math.imul(hash ^ ((code >>> 8) & 0xff), 0x01000193) >>> 0; + } + return hash >>> 0; +} + +/** + * Opaque, stable scope key for a user + optional organization pair. The + * client also fences remote snapshots on the local auth epoch, so the epoch + * deliberately does not enter this key. + */ +export function buildOpaqueScopeKey(input: { + userId: string; + organizationId: string | null; +}): string { + // Length-delimited by a NUL separator so `user=ab,org=c` and `user=a,org=bc` + // cannot hash to the same key. + const joined = `${input.userId}\u0000${input.organizationId ?? ''}`; + const hash = fnv1a32(joined); + return hash.toString(16).padStart(8, '0'); +} + +export type BuildGlanceableSnapshotInput = { + sessions: readonly { status: string }[]; + userId: string; + organizationId: string | null; + /** Epoch milliseconds. */ + now: number; + previousRevision?: number; + previousEligibleStartedAt?: string | null; + accountEpoch?: number; + /** Overrides the happy/empty derivation for waiting, stale, expired, signed_out, privacy. */ + status?: GlanceableAgentsSnapshotStatus; +}; + +/** + * Build a snapshot from the current session rows. Revision increases by one + * on every build. `eligibleStartedAt` keeps the previous value while work + * stays eligible, starts at `now` when work becomes eligible, and is null + * otherwise. + */ +export function buildGlanceableSnapshot(input: BuildGlanceableSnapshotInput): GlanceableAgentsSnapshot { + const counts = countGlanceableSessions(input.sessions); + const eligible = counts.running + counts.needsInput + counts.reconnecting > 0; + const now = input.now; + const updatedAt = new Date(now).toISOString(); + const eligibleStartedAt = eligible ? (input.previousEligibleStartedAt ?? updatedAt) : null; + + return { + schemaVersion: GLANCEABLE_SNAPSHOT_SCHEMA_VERSION, + revision: (input.previousRevision ?? 0) + 1, + updatedAt, + expiresAt: new Date(now + GLANCEABLE_SNAPSHOT_EXPIRY_MS).toISOString(), + scopeKey: buildOpaqueScopeKey({ userId: input.userId, organizationId: input.organizationId }), + ...(input.accountEpoch === undefined ? {} : { accountEpoch: input.accountEpoch }), + organizationBound: typeof input.organizationId === 'string', + status: input.status ?? (eligible ? 'happy' : 'empty'), + running: counts.running, + needsInput: counts.needsInput, + reconnecting: counts.reconnecting, + eligibleStartedAt, + }; +} + +/** True when any eligible count is non-zero. */ +export function isEligibleGlanceableWork(snapshot: GlanceableAgentsSnapshot): boolean { + return snapshot.running + snapshot.needsInput + snapshot.reconnecting > 0; +} + +/** + * True when `incoming` must be discarded in favour of `current`: a strictly + * lower revision, or the same revision with an older `updatedAt`. ISO strings + * from `toISOString()` compare correctly as strings. + */ +export function shouldDiscardGlanceableRevision( + incoming: GlanceableAgentsSnapshot, + current: GlanceableAgentsSnapshot +): boolean { + if (incoming.revision < current.revision) { + return true; + } + if (incoming.revision === current.revision) { + return incoming.updatedAt < current.updatedAt; + } + return false; +} From 44f0caa4654385d602706b75aec1ef27c10e5060 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Fri, 28 Aug 2026 00:07:52 +0200 Subject: [PATCH 2/8] fix(glanceable): gate publisher and persist, fix level-1 CI --- apps/mobile/knip.json | 7 ++- .../mobile/src/lib/auth/auth-context.test.tsx | 9 ++-- .../mobile/src/lib/glanceable/persist.test.ts | 25 +++++++++ apps/mobile/src/lib/glanceable/persist.ts | 11 ++-- .../src/lib/glanceable/publisher.test.ts | 54 +++++++++++++++++++ apps/mobile/src/lib/glanceable/publisher.ts | 10 ++++ 6 files changed, 109 insertions(+), 7 deletions(-) diff --git a/apps/mobile/knip.json b/apps/mobile/knip.json index 58d44efcad..a86f207727 100644 --- a/apps/mobile/knip.json +++ b/apps/mobile/knip.json @@ -10,7 +10,12 @@ "@sentry/cli" ], "ignoreBinaries": ["oxfmt", "oxlint"], - "ignore": ["src/components/ui/**"], + "ignore": ["src/components/ui/**", "src/lib/glanceable/open-agents.ts"], + "ignoreIssues": { + "src/lib/glanceable/presentation.ts": ["exports", "types"], + "src/lib/glanceable/publisher.ts": ["exports"], + "src/lib/glanceable/sink-registry.ts": ["exports", "types"] + }, "postcss": { "config": ["postcss.config.mjs"] }, diff --git a/apps/mobile/src/lib/auth/auth-context.test.tsx b/apps/mobile/src/lib/auth/auth-context.test.tsx index ad3ac09f73..dafe95c348 100644 --- a/apps/mobile/src/lib/auth/auth-context.test.tsx +++ b/apps/mobile/src/lib/auth/auth-context.test.tsx @@ -631,7 +631,9 @@ describe('stale sign-in continuation', () => { expect(getCtx().sessionEnded).toBe(true); expect(getCtx().isSigningOut).toBe(true); const { queryClient: queryClientMock } = await import('@/lib/query-client'); - expect(vi.mocked(queryClientMock.clear)).toHaveBeenCalledTimes(1); + // The account-switch path in signIn clears once, and the sign-out teardown + // clears a second time. + expect(vi.mocked(queryClientMock.clear)).toHaveBeenCalledTimes(2); unmount(); }); @@ -1170,8 +1172,9 @@ describe('auth-transition queue and sign-out failure matrix', () => { }); // Whole-body FIFO: the sign-out's teardown (including the query-client - // clear) settled before the sign-in's credential write ran. - expect(clearMock).toHaveBeenCalledTimes(1); + // clear) settled before the sign-in's credential write ran; the sign-in + // account-switch path then clears a second time after its credential write. + expect(clearMock).toHaveBeenCalledTimes(2); const clearOrder = clearMock.mock.invocationCallOrder[0]; const setOrder = hoisted.secureStore.setItemAsync.mock.invocationCallOrder[0]; expect(clearOrder).toBeLessThan(setOrder); diff --git a/apps/mobile/src/lib/glanceable/persist.test.ts b/apps/mobile/src/lib/glanceable/persist.test.ts index 86491fa7fe..c45b684da2 100644 --- a/apps/mobile/src/lib/glanceable/persist.test.ts +++ b/apps/mobile/src/lib/glanceable/persist.test.ts @@ -7,6 +7,7 @@ import { import { _resetGlanceablePersistForTests, + _setLastGlanceableSnapshotForTests, _setSecureStoreForTests, getLastGlanceableSnapshot, getLocalScopeKey, @@ -103,6 +104,30 @@ describe('restorePersistedGlanceable', () => { expect(getLastGlanceableSnapshot()).toBeNull(); }); + it('does not clobber an in-memory state already set before the restore read', async () => { + // A stale persisted record from a prior session sits on disk. + const stale = snapshotFor([{ status: 'busy' }]); + store.set(SNAPSHOT_KEY, JSON.stringify(stale)); + store.set(SCOPE_KEY, 'stale-scope'); + + // A logout blank landed in memory before restore started (a remount, not a + // JS restart). Its SecureStore mirror is not part of this test, so the + // disk still holds the stale record. + const blank = buildGlanceableSnapshot({ + sessions: [], + userId: 'u1', + organizationId: null, + now: NOW, + status: 'signed_out', + }); + _setLastGlanceableSnapshotForTests(blank); + + await restorePersistedGlanceable(); + + expect(getLastGlanceableSnapshot()).toEqual(blank); + expect(getLocalScopeKey()).toBe(blank.scopeKey); + }); + it('restores a schema-valid stored record', async () => { const stored = snapshotFor([{ status: 'busy' }]); store.set(SNAPSHOT_KEY, JSON.stringify(stored)); diff --git a/apps/mobile/src/lib/glanceable/persist.ts b/apps/mobile/src/lib/glanceable/persist.ts index 5263448f54..a3939f15b8 100644 --- a/apps/mobile/src/lib/glanceable/persist.ts +++ b/apps/mobile/src/lib/glanceable/persist.ts @@ -77,7 +77,9 @@ function parseStoredSnapshot(raw: string): GlanceableAgentsSnapshot | null { /** * Restore the in-memory state from SecureStore after a JS restart. Best * effort: a failed read keeps the null in-memory state. A live write during - * the read owns the state, so the stale persisted record is skipped. + * the read owns the state, so the stale persisted record is skipped. An + * already-set in-memory state (a remount) is never overwritten: only a real + * JS restart starts null and must restore from the persisted record. */ export async function restorePersistedGlanceable(): Promise { const startEpoch = persistEpoch; @@ -90,13 +92,16 @@ export async function restorePersistedGlanceable(): Promise { if (persistEpoch !== startEpoch) { return; } - if (rawSnapshot !== null) { + // A remount keeps the module alive, so a logout or privacy blank written + // before restore owns the state and must not be clobbered by a stale disk + // record from the prior session. + if (rawSnapshot !== null && lastSnapshot === null) { const parsed = parseStoredSnapshot(rawSnapshot); if (parsed !== null) { lastSnapshot = parsed; } } - if (rawScope !== null) { + if (rawScope !== null && localScopeKey === null) { localScopeKey = rawScope; } } catch { diff --git a/apps/mobile/src/lib/glanceable/publisher.test.ts b/apps/mobile/src/lib/glanceable/publisher.test.ts index a470bdf8c9..fedbba8203 100644 --- a/apps/mobile/src/lib/glanceable/publisher.test.ts +++ b/apps/mobile/src/lib/glanceable/publisher.test.ts @@ -231,6 +231,60 @@ describe('GlanceablePublisher', () => { publisher.dispose(); }); + it('cancels a pending coalesced emit on a fetch error', () => { + vi.useFakeTimers(); + const { sink, calls } = makeSink(); + const publisher = new GlanceablePublisher({ sinks: [sink], now: () => NOW, coalesceMs: 1000 }); + publisher.handleSessions([{ status: 'busy' }], PUB_CTX); + publisher.handleSessions([{ status: 'busy' }, { status: 'busy' }], PUB_CTX); + publisher.handleFetchError(PUB_CTX); + vi.advanceTimersByTime(1000); + // The pre-error happy emit must not fire after the stale republish. + expect(count(calls, 'startOrUpdate')).toBe(1); + expect(lastSnapshot(calls, 'publish').status).toBe('stale'); + publisher.dispose(); + }); + + it('does not apply a snapshot after a terminal blank', () => { + const { sink, calls } = makeSink(); + const publisher = new GlanceablePublisher({ + sinks: [sink], + now: () => NOW, + terminalBlankEpoch: getTerminalBlankEpoch, + }); + writeSignedOutSnapshotAndEnd(); + publisher.applySnapshot(snapshotFor([{ status: 'busy' }], NOW), PUB_CTX); + expect(count(calls, 'startOrUpdate')).toBe(0); + expect(count(calls, 'publish')).toBe(0); + publisher.dispose(); + }); + + it('cancels a pending coalesced emit when a newer snapshot applies', () => { + vi.useFakeTimers(); + const { sink, calls } = makeSink(); + const publisher = new GlanceablePublisher({ sinks: [sink], now: () => NOW, coalesceMs: 1000 }); + publisher.handleSessions([{ status: 'busy' }], PUB_CTX); + publisher.handleSessions([{ status: 'busy' }, { status: 'busy' }], PUB_CTX); + publisher.applySnapshot(snapshotFor([{ status: 'busy' }], NOW + 1, 2), PUB_CTX); + vi.advanceTimersByTime(1000); + // Only the applied snapshot emits; the older coalesced happy update must not. + expect(count(calls, 'startOrUpdate')).toBe(2); + expect(lastSnapshot(calls, 'startOrUpdate').running).toBe(1); + publisher.dispose(); + }); + + it('cancels a pending terminal when a newer snapshot applies', () => { + vi.useFakeTimers(); + const { sink, calls } = makeSink(); + const publisher = new GlanceablePublisher({ sinks: [sink], now: () => NOW }); + publisher.handleSessions([{ status: 'busy' }], PUB_CTX); + publisher.handleSessions([{ status: 'idle' }], PUB_CTX); + publisher.applySnapshot(snapshotFor([{ status: 'busy' }], NOW + 1, 1), PUB_CTX); + vi.advanceTimersByTime(8000); + expect(count(calls, 'endImmediate')).toBe(0); + publisher.dispose(); + }); + it('keeps the revision monotonic when seeded from an initial snapshot', () => { const { sink, calls } = makeSink(); // Seeded with revision 42; the next snapshot must be 43. diff --git a/apps/mobile/src/lib/glanceable/publisher.ts b/apps/mobile/src/lib/glanceable/publisher.ts index af203542d7..c4826b4435 100644 --- a/apps/mobile/src/lib/glanceable/publisher.ts +++ b/apps/mobile/src/lib/glanceable/publisher.ts @@ -149,6 +149,9 @@ export class GlanceablePublisher { if (this.applyExpiry(now, ctx) || this.current === null) { return; } + // A fetch error supersedes any pending coalesced happy emit: otherwise the + // pre-error snapshot would fire later and overwrite the stale counts. + this.cancelCoalesce(); const snapshot = withStatus(this.current, 'stale', now); this.publish(snapshot); this.current = snapshot; @@ -159,9 +162,16 @@ export class GlanceablePublisher { * are discarded; the local account epoch is applied by the caller. */ applySnapshot(incoming: GlanceableAgentsSnapshot, ctx: GlanceablePublisherContext): void { + if (this.isGated()) { + return; + } if (this.current !== null && shouldDiscardGlanceableRevision(incoming, this.current)) { return; } + // A late background delivery supersedes a pending coalesced emit and any + // pending 8 s terminal, so neither can fire after the newer snapshot. + this.cancelCoalesce(); + this.cancelTerminal(); if (isEligibleGlanceableWork(incoming)) { this.emit(incoming, ctx); this.activityStarted = true; From 4083fdbf74e4ead7008f9ab82f729393b7497398 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Fri, 28 Aug 2026 01:11:07 +0200 Subject: [PATCH 3/8] style(app-shared): format glanceable snapshot files --- .../src/glanceable-agents-snapshot.test.ts | 14 ++++++++++++-- .../app-shared/src/glanceable-agents-snapshot.ts | 8 ++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/app-shared/src/glanceable-agents-snapshot.test.ts b/packages/app-shared/src/glanceable-agents-snapshot.test.ts index c46f20b5c2..c59de79ca7 100644 --- a/packages/app-shared/src/glanceable-agents-snapshot.test.ts +++ b/packages/app-shared/src/glanceable-agents-snapshot.test.ts @@ -168,8 +168,18 @@ describe('buildGlanceableSnapshot', () => { describe('isEligibleGlanceableWork and revision discard', () => { it('reports eligibility from the three counts', () => { - const empty = buildGlanceableSnapshot({ sessions: [], userId: 'u1', organizationId: null, now: NOW }); - const busy = buildGlanceableSnapshot({ sessions: [{ status: 'busy' }], userId: 'u1', organizationId: null, now: NOW }); + const empty = buildGlanceableSnapshot({ + sessions: [], + userId: 'u1', + organizationId: null, + now: NOW, + }); + const busy = buildGlanceableSnapshot({ + sessions: [{ status: 'busy' }], + userId: 'u1', + organizationId: null, + now: NOW, + }); expect(isEligibleGlanceableWork(empty)).toBe(false); expect(isEligibleGlanceableWork(busy)).toBe(true); }); diff --git a/packages/app-shared/src/glanceable-agents-snapshot.ts b/packages/app-shared/src/glanceable-agents-snapshot.ts index 4a492831cd..e7498e80f1 100644 --- a/packages/app-shared/src/glanceable-agents-snapshot.ts +++ b/packages/app-shared/src/glanceable-agents-snapshot.ts @@ -66,9 +66,7 @@ export type GlanceableCounts = { * `question`/`permission` → needs-input, `retry` → reconnecting. `idle` and * any unknown status are ignored. Do not call `isCompletedStatus` here. */ -export function countGlanceableSessions( - sessions: readonly { status: string }[] -): GlanceableCounts { +export function countGlanceableSessions(sessions: readonly { status: string }[]): GlanceableCounts { let running = 0; let needsInput = 0; let reconnecting = 0; @@ -140,7 +138,9 @@ export type BuildGlanceableSnapshotInput = { * stays eligible, starts at `now` when work becomes eligible, and is null * otherwise. */ -export function buildGlanceableSnapshot(input: BuildGlanceableSnapshotInput): GlanceableAgentsSnapshot { +export function buildGlanceableSnapshot( + input: BuildGlanceableSnapshotInput +): GlanceableAgentsSnapshot { const counts = countGlanceableSessions(input.sessions); const eligible = counts.running + counts.needsInput + counts.reconnecting > 0; const now = input.now; From 404cdee870db2e9edae5d077af2872619cdaf8ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Fri, 28 Aug 2026 10:43:17 +0200 Subject: [PATCH 4/8] feat(i18n): translate glanceable mobile status keys --- apps/mobile/src/i18n/locales/af.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/am.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/ar.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/az.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/be.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/bg.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/bn.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/bs.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/ca.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/ckb.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/cs.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/cy.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/da.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/de.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/el.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/es.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/et.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/eu.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/fa.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/fi.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/fil.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/fr.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/ga.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/gl.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/gu.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/ha.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/he.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/hi.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/hr.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/ht.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/hu.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/hy.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/id.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/ig.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/is.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/it.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/ja.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/ka.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/kk.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/km.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/kn.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/ko.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/lo.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/lt.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/lv.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/mg.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/mi.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/mk.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/ml.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/mn.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/mr.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/ms.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/mt.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/my.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/nb.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/ne.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/nl.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/om.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/or.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/pa.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/pl.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/ps.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/pt-BR.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/pt.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/ro.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/ru.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/si.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/sk.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/sl.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/so.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/sq.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/sr.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/sv.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/sw.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/ta.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/te.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/th.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/tr.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/uk.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/ur.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/uz.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/vi.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/yo.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/zh-Hans.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/zh-Hant.json | 15 +++++++++++++++ apps/mobile/src/i18n/locales/zu.json | 15 +++++++++++++++ 86 files changed, 1290 insertions(+) diff --git a/apps/mobile/src/i18n/locales/af.json b/apps/mobile/src/i18n/locales/af.json index bf86dbd3d4..cf670472a7 100644 --- a/apps/mobile/src/i18n/locales/af.json +++ b/apps/mobile/src/i18n/locales/af.json @@ -3191,5 +3191,20 @@ "historyRetry": "Kon nie kletsgeskiedenis laai nie.", "catalogRetry": "Kon nie modelle laai nie", "sendError": "Kon nie die boodskap stuur nie. Probeer asseblief weer." + }, + "glanceable": { + "waiting": "Dateer agente op", + "empty": "Geen werk aan die gang nie", + "stale": "Kan nie nou opdateer nie", + "expired": "Status het verval", + "signedOut": "Meld aan om agente te sien", + "privacy": "Agente versteek", + "openAgents": "Maak agente oop", + "running": "LOOP", + "needsInput": "benodig invoer", + "reconnecting": "Verbind tans weer", + "channelName": "Aktiewe agente", + "activityKitDisabledTitle": "Regstreekse Aktiwiteite is af", + "activityKitDisabledBody": "Skakel Regstreekse Aktiwiteite in Instellings aan om Aktiewe Agente op die Sluitskerm te sien." } } diff --git a/apps/mobile/src/i18n/locales/am.json b/apps/mobile/src/i18n/locales/am.json index 05d88c7601..6eb270f961 100644 --- a/apps/mobile/src/i18n/locales/am.json +++ b/apps/mobile/src/i18n/locales/am.json @@ -3191,5 +3191,20 @@ "historyRetry": "የውይይት ታሪክ መጫን አልተቻለም።", "catalogRetry": "ሞዴሎችን መጫን አልተቻለም", "sendError": "መልእክቱን መላክ አልተቻለም። እንደገና ይሞክሩ።" + }, + "glanceable": { + "waiting": "ወኪሎችን በማዘመን ላይ", + "empty": "በሂደት ላይ ያለ ስራ የለም", + "stale": "አሁን ማዘመን አይቻልም", + "expired": "የሁኔታው ጊዜ አልፏል", + "signedOut": "ወኪሎችን ለማየት ይግቡ", + "privacy": "ወኪሎች ተደብቀዋል", + "openAgents": "ወኪሎችን ይክፈቱ", + "running": "በስራ ላይ", + "needsInput": "ግብዓት ይፈልጋል", + "reconnecting": "እንደገና በመገናኘት ላይ", + "channelName": "ንቁ ወኪሎች", + "activityKitDisabledTitle": "የቀጥታ እንቅስቃሴዎች ጠፍተዋል", + "activityKitDisabledBody": "ንቁ ወኪሎችን በመቆለፊያ ማያ ገጽ ላይ ለማየት በቅንብሮች ውስጥ የቀጥታ እንቅስቃሴዎችን ያብሩ።" } } diff --git a/apps/mobile/src/i18n/locales/ar.json b/apps/mobile/src/i18n/locales/ar.json index aca86d9e3a..baaef6572f 100644 --- a/apps/mobile/src/i18n/locales/ar.json +++ b/apps/mobile/src/i18n/locales/ar.json @@ -3275,5 +3275,20 @@ "historyRetry": "تعذّر تحميل سجل المحادثة.", "catalogRetry": "تعذّر تحميل النماذج", "sendError": "تعذّر إرسال الرسالة. حاول مرة أخرى." + }, + "glanceable": { + "waiting": "جارٍ تحديث الوكلاء", + "empty": "لا يوجد عمل قيد التنفيذ", + "stale": "يتعذّر التحديث الآن", + "expired": "انتهت صلاحية الحالة", + "signedOut": "سجّل الدخول لرؤية الوكلاء", + "privacy": "الوكلاء مخفيون", + "openAgents": "فتح الوكلاء", + "running": "قيد التشغيل", + "needsInput": "يتطلب إدخالًا", + "reconnecting": "جارٍ إعادة الاتصال", + "channelName": "الوكلاء النشطون", + "activityKitDisabledTitle": "الأنشطة المباشرة متوقفة", + "activityKitDisabledBody": "فعّل الأنشطة المباشرة في الإعدادات لرؤية الوكلاء النشطين على شاشة القفل." } } diff --git a/apps/mobile/src/i18n/locales/az.json b/apps/mobile/src/i18n/locales/az.json index 616865ed68..760a5f472b 100644 --- a/apps/mobile/src/i18n/locales/az.json +++ b/apps/mobile/src/i18n/locales/az.json @@ -3191,5 +3191,20 @@ "historyRetry": "Söhbət tarixçəsi yüklənə bilmədi.", "catalogRetry": "Modellər yüklənə bilmədi", "sendError": "Mesaj göndərilə bilmədi. Yenidən cəhd edin." + }, + "glanceable": { + "waiting": "Agentlər yenilənir", + "empty": "Davam edən iş yoxdur", + "stale": "Hazırda yeniləmək mümkün deyil", + "expired": "Statusun müddəti bitib", + "signedOut": "Agentləri görmək üçün daxil olun", + "privacy": "Agentlər gizlədilib", + "openAgents": "Agentləri açın", + "running": "İŞLƏYİR", + "needsInput": "GİRİŞ TƏLƏB OLUNUR", + "reconnecting": "Yenidən qoşulur", + "channelName": "Aktiv agentlər", + "activityKitDisabledTitle": "Canlı fəaliyyətlər söndürülüb", + "activityKitDisabledBody": "Kilid ekranında aktiv agentləri görmək üçün Parametrlərdə Canlı fəaliyyətləri aktivləşdirin." } } diff --git a/apps/mobile/src/i18n/locales/be.json b/apps/mobile/src/i18n/locales/be.json index 6ffed2ad89..dd9e35eea3 100644 --- a/apps/mobile/src/i18n/locales/be.json +++ b/apps/mobile/src/i18n/locales/be.json @@ -3233,5 +3233,20 @@ "historyRetry": "Не атрымалася загрузіць гісторыю чата.", "catalogRetry": "Не атрымалася загрузіць мадэлі", "sendError": "Не атрымалася адправіць паведамленне. Паспрабуйце яшчэ раз." + }, + "glanceable": { + "waiting": "Абнаўленне агентаў", + "empty": "Няма працы ў выкананні", + "stale": "Зараз немагчыма абнавіць", + "expired": "Тэрмін дзеяння статусу скончыўся", + "signedOut": "Увайдзіце, каб бачыць агентаў", + "privacy": "Агенты схаваны", + "openAgents": "Адкрыць агентаў", + "running": "ПРАЦУЕ", + "needsInput": "патрабуецца ўвод", + "reconnecting": "Паўторнае падключэнне", + "channelName": "Актыўныя агенты", + "activityKitDisabledTitle": "Жывыя дзеянні выключаны", + "activityKitDisabledBody": "Уключыце «Жывыя дзеянні» ў «Наладах», каб бачыць актыўных агентаў на экране блакіроўкі." } } diff --git a/apps/mobile/src/i18n/locales/bg.json b/apps/mobile/src/i18n/locales/bg.json index 5e5ddab41e..bd69c06cff 100644 --- a/apps/mobile/src/i18n/locales/bg.json +++ b/apps/mobile/src/i18n/locales/bg.json @@ -3191,5 +3191,20 @@ "historyRetry": "Не можа да се зареди историята на чата.", "catalogRetry": "Не може да се заредят моделите", "sendError": "Не можахме да изпратим съобщението. Опитайте отново." + }, + "glanceable": { + "waiting": "Актуализиране на агентите", + "empty": "Няма работа в ход", + "stale": "Не може да се актуализира сега", + "expired": "Статусът е изтекъл", + "signedOut": "Влезте, за да видите агентите", + "privacy": "Агентите са скрити", + "openAgents": "Отворете агентите", + "running": "Изпълнява се", + "needsInput": "изисква въвеждане", + "reconnecting": "Повторно свързване", + "channelName": "Активни агенти", + "activityKitDisabledTitle": "Дейностите на живо са изключени", + "activityKitDisabledBody": "Включете Дейности на живо в Настройки, за да виждате активните агенти на заключения екран." } } diff --git a/apps/mobile/src/i18n/locales/bn.json b/apps/mobile/src/i18n/locales/bn.json index 5cef607455..c4987395e3 100644 --- a/apps/mobile/src/i18n/locales/bn.json +++ b/apps/mobile/src/i18n/locales/bn.json @@ -3191,5 +3191,20 @@ "historyRetry": "চ্যাট ইতিহাস লোড করা যায়নি।", "catalogRetry": "মডেল লোড করা যায়নি", "sendError": "বার্তাটি পাঠানো যায়নি। আবার চেষ্টা করুন।" + }, + "glanceable": { + "waiting": "এজেন্টগুলি আপডেট করা হচ্ছে", + "empty": "কোনো কাজ চলছে না", + "stale": "এখন আপডেট করা যাচ্ছে না", + "expired": "অবস্থার মেয়াদ শেষ হয়েছে", + "signedOut": "এজেন্টগুলি দেখতে সাইন ইন করুন", + "privacy": "এজেন্টগুলি লুকানো আছে", + "openAgents": "এজেন্টগুলি খুলুন", + "running": "চলছে", + "needsInput": "ইনপুট প্রয়োজন", + "reconnecting": "পুনরায় সংযোগ করা হচ্ছে", + "channelName": "সক্রিয় এজেন্ট", + "activityKitDisabledTitle": "সরাসরি কার্যকলাপ বন্ধ আছে", + "activityKitDisabledBody": "লক স্ক্রিনে সক্রিয় এজেন্টগুলি দেখতে সেটিংসে সরাসরি কার্যকলাপ চালু করুন।" } } diff --git a/apps/mobile/src/i18n/locales/bs.json b/apps/mobile/src/i18n/locales/bs.json index 8706dd3800..df8241f41a 100644 --- a/apps/mobile/src/i18n/locales/bs.json +++ b/apps/mobile/src/i18n/locales/bs.json @@ -3212,5 +3212,20 @@ "historyRetry": "Nije moguće učitati istoriju razgovora.", "catalogRetry": "Nije moguće učitati modele", "sendError": "Poruka nije mogla biti poslana. Pokušajte ponovo." + }, + "glanceable": { + "waiting": "Ažuriranje agenata", + "empty": "Nema rada u toku", + "stale": "Trenutno nije moguće ažurirati", + "expired": "Status je istekao", + "signedOut": "Prijavite se da biste vidjeli agente", + "privacy": "Agenti su skriveni", + "openAgents": "Otvorite agente", + "running": "RADI", + "needsInput": "treba unos", + "reconnecting": "Ponovno povezivanje", + "channelName": "Aktivni agenti", + "activityKitDisabledTitle": "Aktivnosti uživo su isključene", + "activityKitDisabledBody": "Uključite aktivnosti uživo u Postavkama da biste vidjeli aktivne agente na zaključanom ekranu." } } diff --git a/apps/mobile/src/i18n/locales/ca.json b/apps/mobile/src/i18n/locales/ca.json index ebeb0aa7f2..8e1ae0ba1b 100644 --- a/apps/mobile/src/i18n/locales/ca.json +++ b/apps/mobile/src/i18n/locales/ca.json @@ -3212,5 +3212,20 @@ "historyRetry": "No s'ha pogut carregar l'historial del xat.", "catalogRetry": "No s'han pogut carregar els models", "sendError": "No s'ha pogut enviar el missatge. Torna-ho a provar." + }, + "glanceable": { + "waiting": "Actualitzant els agents", + "empty": "No hi ha cap feina en curs", + "stale": "Ara no es pot actualitzar", + "expired": "Estat caducat", + "signedOut": "Inicia la sessió per veure els agents", + "privacy": "Agents ocults", + "openAgents": "Obre els agents", + "running": "EN EXECUCIÓ", + "needsInput": "requereix entrada", + "reconnecting": "Reconnectant", + "channelName": "Agents actius", + "activityKitDisabledTitle": "Les activitats en directe estan desactivades", + "activityKitDisabledBody": "Activa les activitats en directe a Configuració per veure els agents actius a la pantalla de bloqueig." } } diff --git a/apps/mobile/src/i18n/locales/ckb.json b/apps/mobile/src/i18n/locales/ckb.json index cd1202e9ca..b162480f86 100644 --- a/apps/mobile/src/i18n/locales/ckb.json +++ b/apps/mobile/src/i18n/locales/ckb.json @@ -3191,5 +3191,20 @@ "historyRetry": "نەتوانرا مێژووی گفتوگۆ بار بکرێت.", "catalogRetry": "نەتوانرا مۆدێلەکان بار بکرێن", "sendError": "شکستی هێنا لە ناردنی پەیام. تکایە دووبارە هەوڵ بدە." + }, + "glanceable": { + "waiting": "لە نوێکردنەوەی ئەجێنتەکاندایە", + "empty": "هیچ کارێک لە ئەنجامداندان نییە", + "stale": "ئێستا ناتوانرێت نوێ بکرێتەوە", + "expired": "دۆخەکە بەسەرچووە", + "signedOut": "بچۆ ژوورەوە بۆ بینینی ئەجێنتەکان", + "privacy": "ئەجێنتەکان شاراونەتەوە", + "openAgents": "کردنەوەی ئەجێنتەکان", + "running": "لە کاردایە", + "needsInput": "پێویستی بە داخڵکردن", + "reconnecting": "لە پەیوەستبوونەوەدایە", + "channelName": "ئەجێنتە چالاکەکان", + "activityKitDisabledTitle": "چالاکییە ڕاستەوخۆکان ناچالاکن", + "activityKitDisabledBody": "چالاکییە ڕاستەوخۆکان لە ڕێکخستنەکان چالاک بکە بۆ بینینی ئەجێنتە چالاکەکان لە شاشەی قوفڵ." } } diff --git a/apps/mobile/src/i18n/locales/cs.json b/apps/mobile/src/i18n/locales/cs.json index e1d3e860dd..689e0cef35 100644 --- a/apps/mobile/src/i18n/locales/cs.json +++ b/apps/mobile/src/i18n/locales/cs.json @@ -3233,5 +3233,20 @@ "historyRetry": "Historie chatu se nepodařila načíst.", "catalogRetry": "Modely se nepodařilo načíst", "sendError": "Zprávu se nepodařilo odeslat. Zkuste to prosím znovu." + }, + "glanceable": { + "waiting": "Aktualizace agentů", + "empty": "Žádná probíhající práce", + "stale": "Nyní nelze aktualizovat", + "expired": "Platnost stavu vypršela", + "signedOut": "Přihlaste se pro zobrazení agentů", + "privacy": "Agenti jsou skrytí", + "openAgents": "Otevřít agenty", + "running": "BĚŽÍ", + "needsInput": "vyžaduje vstup", + "reconnecting": "Obnovování připojení", + "channelName": "Aktivní agenti", + "activityKitDisabledTitle": "Živé aktivity jsou vypnuté", + "activityKitDisabledBody": "Zapněte Živé aktivity v Nastavení, abyste viděli aktivní agenty na zamknuté obrazovce." } } diff --git a/apps/mobile/src/i18n/locales/cy.json b/apps/mobile/src/i18n/locales/cy.json index b3502cd769..ae034409c3 100644 --- a/apps/mobile/src/i18n/locales/cy.json +++ b/apps/mobile/src/i18n/locales/cy.json @@ -3275,5 +3275,20 @@ "historyRetry": "Ni ellid llwytho hanes y sgwrs.", "catalogRetry": "Methodd llwytho modelau", "sendError": "Methodd anfon neges. Ceisiwch eto." + }, + "glanceable": { + "waiting": "Yn diweddaru asiantau", + "empty": "Dim gwaith ar y gweill", + "stale": "Methu diweddaru nawr", + "expired": "Mae'r statws wedi dod i ben", + "signedOut": "Mewngofnodwch i weld asiantau", + "privacy": "Asiantau wedi'u cuddio", + "openAgents": "Agorwch asiantau", + "running": "YN RHEDEG", + "needsInput": "angen mewnbwn", + "reconnecting": "Yn ailgysylltu", + "channelName": "Asiantau gweithredol", + "activityKitDisabledTitle": "Mae Gweithgareddau Byw wedi'u diffodd", + "activityKitDisabledBody": "Trowch Weithgareddau Byw ymlaen yn Gosodiadau i weld Asiantau gweithredol ar y Sgrin Glo." } } diff --git a/apps/mobile/src/i18n/locales/da.json b/apps/mobile/src/i18n/locales/da.json index 4a71b1e63d..e4e3101fc3 100644 --- a/apps/mobile/src/i18n/locales/da.json +++ b/apps/mobile/src/i18n/locales/da.json @@ -3191,5 +3191,20 @@ "historyRetry": "Kunne ikke indlæse chathistorik.", "catalogRetry": "Kunne ikke indlæse modeller", "sendError": "Kunne ikke sende beskeden. Prøv igen." + }, + "glanceable": { + "waiting": "Opdaterer agenter", + "empty": "Intet arbejde i gang", + "stale": "Kan ikke opdatere nu", + "expired": "Status er udløbet", + "signedOut": "Log ind for at se agenter", + "privacy": "Agenter er skjult", + "openAgents": "Åbn agenter", + "running": "KØRER", + "needsInput": "kræver input", + "reconnecting": "Genopretter forbindelsen", + "channelName": "Aktive agenter", + "activityKitDisabledTitle": "Liveaktiviteter er slået fra", + "activityKitDisabledBody": "Slå Liveaktiviteter til i Indstillinger for at se aktive agenter på låseskærmen." } } diff --git a/apps/mobile/src/i18n/locales/de.json b/apps/mobile/src/i18n/locales/de.json index 9dff50c07d..0d65fb86b4 100644 --- a/apps/mobile/src/i18n/locales/de.json +++ b/apps/mobile/src/i18n/locales/de.json @@ -3191,5 +3191,20 @@ "historyRetry": "Chatverlauf konnte nicht geladen werden.", "catalogRetry": "Modelle konnten nicht geladen werden", "sendError": "Die Nachricht konnte nicht gesendet werden. Versuchen Sie es erneut." + }, + "glanceable": { + "waiting": "Agenten werden aktualisiert", + "empty": "Keine laufende Arbeit", + "stale": "Aktualisierung derzeit nicht möglich", + "expired": "Status abgelaufen", + "signedOut": "Melde dich an, um Agenten zu sehen", + "privacy": "Agenten ausgeblendet", + "openAgents": "Agenten öffnen", + "running": "LÄUFT", + "needsInput": "Eingabe erforderlich", + "reconnecting": "Verbindung wird wiederhergestellt", + "channelName": "Aktive Agenten", + "activityKitDisabledTitle": "Live-Aktivitäten sind deaktiviert", + "activityKitDisabledBody": "Aktiviere Live-Aktivitäten in den Einstellungen, um aktive Agenten auf dem Sperrbildschirm zu sehen." } } diff --git a/apps/mobile/src/i18n/locales/el.json b/apps/mobile/src/i18n/locales/el.json index e4d1a74c0f..afc450b50f 100644 --- a/apps/mobile/src/i18n/locales/el.json +++ b/apps/mobile/src/i18n/locales/el.json @@ -3191,5 +3191,20 @@ "historyRetry": "Δεν ήταν δυνατή η φόρτωση του ιστορικού συνομιλίας.", "catalogRetry": "Δεν ήταν δυνατή η φόρτωση των μοντέλων", "sendError": "Δεν ήταν δυνατή η αποστολή του μηνύματος. Δοκιμάστε ξανά." + }, + "glanceable": { + "waiting": "Ενημέρωση πρακτόρων", + "empty": "Καμία εργασία σε εξέλιξη", + "stale": "Δεν είναι δυνατή η ενημέρωση τώρα", + "expired": "Η κατάσταση έληξε", + "signedOut": "Συνδεθείτε για να δείτε τους πράκτορες", + "privacy": "Οι πράκτορες είναι κρυφοί", + "openAgents": "Ανοίξτε τους πράκτορες", + "running": "Σε εξέλιξη", + "needsInput": "χρειάζεται είσοδο", + "reconnecting": "Επανασύνδεση", + "channelName": "Ενεργοί πράκτορες", + "activityKitDisabledTitle": "Οι Ζωντανές δραστηριότητες είναι απενεργοποιημένες", + "activityKitDisabledBody": "Ενεργοποιήστε τις Ζωντανές δραστηριότητες στις Ρυθμίσεις για να δείτε τους ενεργούς πράκτορες στην Οθόνη κλειδώματος." } } diff --git a/apps/mobile/src/i18n/locales/es.json b/apps/mobile/src/i18n/locales/es.json index 56f4c5c1a7..8eeb9eaa2d 100644 --- a/apps/mobile/src/i18n/locales/es.json +++ b/apps/mobile/src/i18n/locales/es.json @@ -3212,5 +3212,20 @@ "historyRetry": "No se pudo cargar el historial del chat.", "catalogRetry": "No se pudieron cargar los modelos", "sendError": "No se pudo enviar el mensaje. Inténtalo de nuevo." + }, + "glanceable": { + "waiting": "Actualizando agentes", + "empty": "No hay trabajo en curso", + "stale": "No se puede actualizar ahora", + "expired": "Estado caducado", + "signedOut": "Inicia sesión para ver los agentes", + "privacy": "Agentes ocultos", + "openAgents": "Abrir agentes", + "running": "EN EJECUCIÓN", + "needsInput": "requiere entrada", + "reconnecting": "Reconectando", + "channelName": "Agentes activos", + "activityKitDisabledTitle": "Las actividades en directo están desactivadas", + "activityKitDisabledBody": "Activa las actividades en directo en Ajustes para ver los agentes activos en la pantalla de bloqueo." } } diff --git a/apps/mobile/src/i18n/locales/et.json b/apps/mobile/src/i18n/locales/et.json index cf72bc1717..bcc271ae98 100644 --- a/apps/mobile/src/i18n/locales/et.json +++ b/apps/mobile/src/i18n/locales/et.json @@ -3191,5 +3191,20 @@ "historyRetry": "Vestluse ajaloo laadimine ei õnnestunud.", "catalogRetry": "Mudeleid ei õnnestunud laadida", "sendError": "Sõnumi saatmine ebaõnnestus. Proovi uuesti." + }, + "glanceable": { + "waiting": "Agentide uuendamine", + "empty": "Ühtegi tööd pole pooleli", + "stale": "Praegu ei saa uuendada", + "expired": "Olek on aegunud", + "signedOut": "Agentide nägemiseks logige sisse", + "privacy": "Agendid on peidetud", + "openAgents": "Avage agendid", + "running": "TÖÖTAB", + "needsInput": "vajab sisendit", + "reconnecting": "Ühenduse taastamine", + "channelName": "Aktiivsed agendid", + "activityKitDisabledTitle": "Reaalajas tegevused on välja lülitatud", + "activityKitDisabledBody": "Lülitage seadetes reaalajas tegevused sisse, et näha aktiivseid agente lukustuskuval." } } diff --git a/apps/mobile/src/i18n/locales/eu.json b/apps/mobile/src/i18n/locales/eu.json index bff8407646..69401ac1c6 100644 --- a/apps/mobile/src/i18n/locales/eu.json +++ b/apps/mobile/src/i18n/locales/eu.json @@ -3191,5 +3191,20 @@ "historyRetry": "Ezin izan da txat-historia kargatu.", "catalogRetry": "Modeloak ezin izan dira kargatu", "sendError": "Ezin izan da mezua bidali. Saiatu berriro." + }, + "glanceable": { + "waiting": "Agenteak eguneratzen", + "empty": "Ez dago lanik abian", + "stale": "Ezin da orain eguneratu", + "expired": "Egoera iraungi da", + "signedOut": "Hasi saioa agenteak ikusteko", + "privacy": "Agenteak ezkutatuta", + "openAgents": "Ireki agenteak", + "running": "Exekutatzen", + "needsInput": "sarreraren zain", + "reconnecting": "Berriro konektatzen", + "channelName": "Agente aktiboak", + "activityKitDisabledTitle": "Zuzeneko jarduerak desaktibatuta daude", + "activityKitDisabledBody": "Aktibatu Zuzeneko jarduerak Ezarpenetan, Agente aktiboak Blokeo-pantailan ikusteko." } } diff --git a/apps/mobile/src/i18n/locales/fa.json b/apps/mobile/src/i18n/locales/fa.json index 769996caec..4742b0a89b 100644 --- a/apps/mobile/src/i18n/locales/fa.json +++ b/apps/mobile/src/i18n/locales/fa.json @@ -3191,5 +3191,20 @@ "historyRetry": "تاریخچه گفتگو بارگذاری نشد.", "catalogRetry": "بارگذاری مدل‌ها ناموفق بود", "sendError": "ارسال پیام ممکن نشد. دوباره تلاش کنید." + }, + "glanceable": { + "waiting": "در حال به‌روزرسانی عامل‌ها", + "empty": "هیچ کاری در حال انجام نیست", + "stale": "اکنون به‌روزرسانی ممکن نیست", + "expired": "وضعیت منقضی شد", + "signedOut": "برای دیدن عامل‌ها وارد شوید", + "privacy": "عامل‌ها پنهان هستند", + "openAgents": "عامل‌ها را باز کنید", + "running": "در حال اجرا", + "needsInput": "نیاز به ورودی", + "reconnecting": "در حال اتصال مجدد", + "channelName": "عامل‌های فعال", + "activityKitDisabledTitle": "فعالیت‌های زنده خاموش هستند", + "activityKitDisabledBody": "برای دیدن عامل‌های فعال در صفحه قفل، فعالیت‌های زنده را در تنظیمات روشن کنید." } } diff --git a/apps/mobile/src/i18n/locales/fi.json b/apps/mobile/src/i18n/locales/fi.json index 7b2db4bbad..bec672d01b 100644 --- a/apps/mobile/src/i18n/locales/fi.json +++ b/apps/mobile/src/i18n/locales/fi.json @@ -3191,5 +3191,20 @@ "historyRetry": "Keskusteluhistoriaa ei voitu ladata.", "catalogRetry": "Malleja ei voitu ladata", "sendError": "Viestiä ei voitu lähettää. Yritä uudelleen." + }, + "glanceable": { + "waiting": "Päivitetään agentteja", + "empty": "Ei keskeneräisiä töitä", + "stale": "Päivitys ei onnistu nyt", + "expired": "Tila vanhentunut", + "signedOut": "Kirjaudu sisään nähdäksesi agentit", + "privacy": "Agentit piilotettu", + "openAgents": "Avaa agentit", + "running": "KÄYNNISSÄ", + "needsInput": "vaatii syötettä", + "reconnecting": "Yhdistetään uudelleen", + "channelName": "Aktiiviset agentit", + "activityKitDisabledTitle": "Live-aktiviteetit ovat pois päältä", + "activityKitDisabledBody": "Ota live-aktiviteetit käyttöön Asetuksissa, niin näet aktiiviset agentit lukitulla näytöllä." } } diff --git a/apps/mobile/src/i18n/locales/fil.json b/apps/mobile/src/i18n/locales/fil.json index 3e097b8437..0aa3f3643a 100644 --- a/apps/mobile/src/i18n/locales/fil.json +++ b/apps/mobile/src/i18n/locales/fil.json @@ -3191,5 +3191,20 @@ "historyRetry": "Hindi ma-load ang kasaysayan ng chat.", "catalogRetry": "Hindi maload ang mga modelo", "sendError": "Nabigong magpadala ng mensahe. Pakisubukang muli." + }, + "glanceable": { + "waiting": "Ina-update ang mga agent", + "empty": "Walang kasalukuyang gawain", + "stale": "Hindi makapag-update ngayon", + "expired": "Nag-expire ang katayuan", + "signedOut": "Mag-sign in para makita ang mga agent", + "privacy": "Nakatago ang mga agent", + "openAgents": "Buksan ang mga agent", + "running": "TUMATAKBO", + "needsInput": "kailangan ng input", + "reconnecting": "Muling kumokonekta", + "channelName": "Mga aktibong agent", + "activityKitDisabledTitle": "Naka-off ang Mga Live na Aktibidad", + "activityKitDisabledBody": "I-on ang Mga Live na Aktibidad sa Mga setting para makita ang Mga aktibong agent sa Naka-lock na Screen." } } diff --git a/apps/mobile/src/i18n/locales/fr.json b/apps/mobile/src/i18n/locales/fr.json index 80522ea5da..a062104a15 100644 --- a/apps/mobile/src/i18n/locales/fr.json +++ b/apps/mobile/src/i18n/locales/fr.json @@ -3212,5 +3212,20 @@ "historyRetry": "Impossible de charger l'historique de la conversation.", "catalogRetry": "Impossible de charger les modèles", "sendError": "Impossible d'envoyer le message. Veuillez réessayer." + }, + "glanceable": { + "waiting": "Mise à jour des agents", + "empty": "Aucun travail en cours", + "stale": "Mise à jour impossible pour le moment", + "expired": "Statut expiré", + "signedOut": "Connectez-vous pour voir les agents", + "privacy": "Agents masqués", + "openAgents": "Ouvrir les agents", + "running": "EN COURS", + "needsInput": "saisie requise", + "reconnecting": "Reconnexion en cours", + "channelName": "Agents actifs", + "activityKitDisabledTitle": "Les activités en direct sont désactivées", + "activityKitDisabledBody": "Activez les activités en direct dans Réglages pour voir les agents actifs sur l'écran verrouillé." } } diff --git a/apps/mobile/src/i18n/locales/ga.json b/apps/mobile/src/i18n/locales/ga.json index 10b741e8f2..0e54003877 100644 --- a/apps/mobile/src/i18n/locales/ga.json +++ b/apps/mobile/src/i18n/locales/ga.json @@ -3254,5 +3254,20 @@ "historyRetry": "Níorbh fhéidir stair an chomhrá a lódáil.", "catalogRetry": "Níorbh fhéidir múnlaí a lódáil", "sendError": "Níorbh fhéidir an teachtaireacht a sheoladh. Bain triail as arís." + }, + "glanceable": { + "waiting": "Gníomhairí á nuashonrú", + "empty": "Níl aon obair ar siúl", + "stale": "Ní féidir nuashonrú anois", + "expired": "Stádas imithe in éag", + "signedOut": "Sínigh isteach chun gníomhairí a fheiceáil", + "privacy": "Gníomhairí i bhfolach", + "openAgents": "Oscail gníomhairí", + "running": "AG RITH", + "needsInput": "teastaíonn ionchur", + "reconnecting": "Ag athcheangal", + "channelName": "Gníomhairí gníomhacha", + "activityKitDisabledTitle": "Tá Gníomhaíochtaí Beo as", + "activityKitDisabledBody": "Cumasaigh Gníomhaíochtaí Beo sna Socruithe chun Gníomhairí gníomhacha a fheiceáil ar an Scáileán Glasála." } } diff --git a/apps/mobile/src/i18n/locales/gl.json b/apps/mobile/src/i18n/locales/gl.json index 0bda69343a..e59937cb68 100644 --- a/apps/mobile/src/i18n/locales/gl.json +++ b/apps/mobile/src/i18n/locales/gl.json @@ -3191,5 +3191,20 @@ "historyRetry": "Non se puido cargar o historial do chat.", "catalogRetry": "Non se puideron cargar os modelos", "sendError": "Non se puido enviar a mensaxe. Téntao de novo." + }, + "glanceable": { + "waiting": "Actualizando axentes", + "empty": "Non hai traballo en curso", + "stale": "Non se pode actualizar agora", + "expired": "Estado caducado", + "signedOut": "Inicia sesión para ver os axentes", + "privacy": "Axentes ocultos", + "openAgents": "Abrir axentes", + "running": "Executando", + "needsInput": "precisa entrada", + "reconnecting": "Reconectando", + "channelName": "Axentes activos", + "activityKitDisabledTitle": "As actividades en directo están desactivadas", + "activityKitDisabledBody": "Activa as actividades en directo en Configuración para ver os axentes activos na pantalla de bloqueo." } } diff --git a/apps/mobile/src/i18n/locales/gu.json b/apps/mobile/src/i18n/locales/gu.json index 7c4480f51a..b23be114ea 100644 --- a/apps/mobile/src/i18n/locales/gu.json +++ b/apps/mobile/src/i18n/locales/gu.json @@ -3191,5 +3191,20 @@ "historyRetry": "વાતચીત ઇતિહાસ લોડ કરી શકાયો નહીં.", "catalogRetry": "મોડેલ લોડ કરી શકાયા નહીં", "sendError": "સંદેશો મોકલી શકાયો નથી. ફરી પ્રયાસ કરો." + }, + "glanceable": { + "waiting": "એજન્ટો અપડેટ થઈ રહ્યા છે", + "empty": "કોઈ કાર્ય ચાલુ નથી", + "stale": "હમણાં અપડેટ કરી શકાતું નથી", + "expired": "સ્થિતિની સમયસીમા સમાપ્ત થઈ", + "signedOut": "એજન્ટો જોવા માટે સાઇન ઇન કરો", + "privacy": "એજન્ટો છુપાવેલા છે", + "openAgents": "એજન્ટો ખોલો", + "running": "ચાલી રહ્યું છે", + "needsInput": "ઇનપુટ જરૂરી", + "reconnecting": "ફરી કનેક્ટ થઈ રહ્યું છે", + "channelName": "સક્રિય એજન્ટો", + "activityKitDisabledTitle": "લાઇવ પ્રવૃત્તિઓ બંધ છે", + "activityKitDisabledBody": "લૉક સ્ક્રીન પર સક્રિય એજન્ટો જોવા માટે સેટિંગ્સમાં લાઇવ પ્રવૃત્તિઓ ચાલુ કરો." } } diff --git a/apps/mobile/src/i18n/locales/ha.json b/apps/mobile/src/i18n/locales/ha.json index 437e4f041e..0707d0bb16 100644 --- a/apps/mobile/src/i18n/locales/ha.json +++ b/apps/mobile/src/i18n/locales/ha.json @@ -3191,5 +3191,20 @@ "historyRetry": "Ba a iya loda tarihin tattaunawa.", "catalogRetry": "An kasa ɗora samfuran", "sendError": "An kasa aika saƙon. Sake gwadawa." + }, + "glanceable": { + "waiting": "Ana sabunta wakilai", + "empty": "Babu aikin da ke gudana", + "stale": "Ba a iya sabuntawa yanzu", + "expired": "Matsayi ya ƙare", + "signedOut": "Shiga don ganin wakilai", + "privacy": "An ɓoye wakilai", + "openAgents": "Buɗe wakilai", + "running": "Ana gudana", + "needsInput": "yana buƙatar bayani", + "reconnecting": "Ana sake haɗawa", + "channelName": "Wakilai da ke aiki", + "activityKitDisabledTitle": "Ayyukan Kai Tsaye suna a kashe", + "activityKitDisabledBody": "Kunna Ayyukan Kai Tsaye a cikin Saituna don ganin Wakilai da ke Aiki a kan Allon Kulle." } } diff --git a/apps/mobile/src/i18n/locales/he.json b/apps/mobile/src/i18n/locales/he.json index fb0e486aa5..f673155d73 100644 --- a/apps/mobile/src/i18n/locales/he.json +++ b/apps/mobile/src/i18n/locales/he.json @@ -3212,5 +3212,20 @@ "historyRetry": "לא ניתן היה לטעון את היסטוריית השיחה.", "catalogRetry": "לא ניתן לטעון דגמים", "sendError": "לא ניתן היה לשלוח את ההודעה. נסה שוב." + }, + "glanceable": { + "waiting": "מעדכן סוכנים", + "empty": "אין עבודה בתהליך", + "stale": "לא ניתן לעדכן כעת", + "expired": "תוקף המצב פג", + "signedOut": "היכנס כדי לראות סוכנים", + "privacy": "הסוכנים מוסתרים", + "openAgents": "פתח סוכנים", + "running": "רץ", + "needsInput": "נדרש קלט", + "reconnecting": "מתחבר מחדש", + "channelName": "סוכנים פעילים", + "activityKitDisabledTitle": "פעילויות בזמן אמת כבויות", + "activityKitDisabledBody": "הפעל פעילויות בזמן אמת בהגדרות כדי לראות סוכנים פעילים במסך הנעילה." } } diff --git a/apps/mobile/src/i18n/locales/hi.json b/apps/mobile/src/i18n/locales/hi.json index c22ccf2567..63e95174cc 100644 --- a/apps/mobile/src/i18n/locales/hi.json +++ b/apps/mobile/src/i18n/locales/hi.json @@ -3191,5 +3191,20 @@ "historyRetry": "चैट इतिहास लोड नहीं किया जा सका।", "catalogRetry": "मॉडल लोड नहीं हो सके", "sendError": "संदेश भेजा नहीं जा सका। फिर से कोशिश करें।" + }, + "glanceable": { + "waiting": "एजेंट अपडेट हो रहे हैं", + "empty": "कोई कार्य प्रगति में नहीं है", + "stale": "अभी अपडेट नहीं हो सकता", + "expired": "स्थिति की समय सीमा समाप्त हो गई", + "signedOut": "एजेंट देखने के लिए साइन इन करें", + "privacy": "एजेंट छिपे हुए हैं", + "openAgents": "एजेंट खोलें", + "running": "चालू", + "needsInput": "इनपुट आवश्यक", + "reconnecting": "फिर से कनेक्ट हो रहा है", + "channelName": "सक्रिय एजेंट", + "activityKitDisabledTitle": "लाइव ऐक्टिविटी बंद हैं", + "activityKitDisabledBody": "लॉक स्क्रीन पर सक्रिय एजेंट देखने के लिए सेटिंग में लाइव ऐक्टिविटी चालू करें।" } } diff --git a/apps/mobile/src/i18n/locales/hr.json b/apps/mobile/src/i18n/locales/hr.json index 10a411fbac..7cb949d93a 100644 --- a/apps/mobile/src/i18n/locales/hr.json +++ b/apps/mobile/src/i18n/locales/hr.json @@ -3212,5 +3212,20 @@ "historyRetry": "Nije moguće učitati povijest razgovora.", "catalogRetry": "Učitavanje modela nije uspjelo", "sendError": "Nije moguće poslati poruku. Pokušajte ponovno." + }, + "glanceable": { + "waiting": "Ažuriranje agenata", + "empty": "Nema rada u tijeku", + "stale": "Trenutačno nije moguće ažurirati", + "expired": "Status je istekao", + "signedOut": "Prijavite se da biste vidjeli agente", + "privacy": "Agenti su skriveni", + "openAgents": "Otvorite agente", + "running": "RADI", + "needsInput": "treba unos", + "reconnecting": "Ponovno povezivanje", + "channelName": "Aktivni agenti", + "activityKitDisabledTitle": "Aktivnosti uživo su isključene", + "activityKitDisabledBody": "Uključite Aktivnosti uživo u Postavkama kako biste vidjeli aktivne agente na zaključanom zaslonu." } } diff --git a/apps/mobile/src/i18n/locales/ht.json b/apps/mobile/src/i18n/locales/ht.json index 22f48c0dfa..92272deb3c 100644 --- a/apps/mobile/src/i18n/locales/ht.json +++ b/apps/mobile/src/i18n/locales/ht.json @@ -3191,5 +3191,20 @@ "historyRetry": "Pa t ka chaje istwa chat la.", "catalogRetry": "Pa t ka chaje modèl yo", "sendError": "Pa t ka voye mesaj la. Tanpri eseye ankò." + }, + "glanceable": { + "waiting": "Ap mete ajans yo ajou", + "empty": "Pa gen travay k ap fèt", + "stale": "Pa ka mete ajou kounye a", + "expired": "Estati a ekspire", + "signedOut": "Konekte pou wè ajans yo", + "privacy": "Ajans yo kache", + "openAgents": "Louvri ajans yo", + "running": "AP KOURI", + "needsInput": "bezwen input", + "reconnecting": "Ap rekonekte", + "channelName": "Ajans aktif yo", + "activityKitDisabledTitle": "Aktivite an dirèk yo fèmen", + "activityKitDisabledBody": "Aktive Aktivite an dirèk nan Paramèt pou wè Ajans aktif yo sou Ekran bloke a." } } diff --git a/apps/mobile/src/i18n/locales/hu.json b/apps/mobile/src/i18n/locales/hu.json index d53fc5dfde..72be12dc20 100644 --- a/apps/mobile/src/i18n/locales/hu.json +++ b/apps/mobile/src/i18n/locales/hu.json @@ -3191,5 +3191,20 @@ "historyRetry": "Nem sikerült betölteni a csevegés előzményeit.", "catalogRetry": "Nem sikerült betölteni a modelleket", "sendError": "Az üzenet küldése nem sikerült. Próbálja újra." + }, + "glanceable": { + "waiting": "Ügynökök frissítése", + "empty": "Nincs folyamatban lévő munka", + "stale": "Most nem frissíthető", + "expired": "Az állapot lejárt", + "signedOut": "Jelentkezzen be az ügynökök megtekintéséhez", + "privacy": "Ügynökök elrejtve", + "openAgents": "Ügynökök megnyitása", + "running": "Folyamatban", + "needsInput": "bemenetet igényel", + "reconnecting": "Újracsatlakozás", + "channelName": "Aktív ügynökök", + "activityKitDisabledTitle": "Az Élő tevékenységek ki vannak kapcsolva", + "activityKitDisabledBody": "Kapcsolja be az Élő tevékenységeket a Beállításokban, hogy az aktív ügynökök megjelenjenek a zárolási képernyőn." } } diff --git a/apps/mobile/src/i18n/locales/hy.json b/apps/mobile/src/i18n/locales/hy.json index bdb556bee2..fd86d5065b 100644 --- a/apps/mobile/src/i18n/locales/hy.json +++ b/apps/mobile/src/i18n/locales/hy.json @@ -3191,5 +3191,20 @@ "historyRetry": "Հնարավոր չեղավ բեռնել զրույցի պատմությունը.", "catalogRetry": "Հնարավոր չեղավ բեռնել մոդելները", "sendError": "Հնարավոր չեղավ ուղարկել հաղորդագրությունը։ Կրկին փորձեք։" + }, + "glanceable": { + "waiting": "Գործակալների թարմացում", + "empty": "Ընթացքի մեջ աշխատանք չկա", + "stale": "Այժմ հնարավոր չէ թարմացնել", + "expired": "Կարգավիճակի ժամկետը լրացել է", + "signedOut": "Մուտք գործեք՝ գործակալներին տեսնելու համար", + "privacy": "Գործակալները թաքցված են", + "openAgents": "Բացեք գործակալները", + "running": "Ընթացքի մեջ է", + "needsInput": "մուտքագրման կարիք ունի", + "reconnecting": "Կրկին միացում", + "channelName": "Ակտիվ գործակալներ", + "activityKitDisabledTitle": "Ուղիղ ակտիվություններն անջատված են", + "activityKitDisabledBody": "Միացրեք «Ուղիղ ակտիվություններ»-ը Կարգավորումներում՝ ակտիվ գործակալներին կողպման էկրանին տեսնելու համար։" } } diff --git a/apps/mobile/src/i18n/locales/id.json b/apps/mobile/src/i18n/locales/id.json index d9d6ec423e..53be7b234d 100644 --- a/apps/mobile/src/i18n/locales/id.json +++ b/apps/mobile/src/i18n/locales/id.json @@ -3191,5 +3191,20 @@ "historyRetry": "Tidak dapat memuat riwayat obrolan.", "catalogRetry": "Tidak dapat memuat model", "sendError": "Tidak dapat mengirim pesan. Coba lagi." + }, + "glanceable": { + "waiting": "Memperbarui agen", + "empty": "Tidak ada pekerjaan yang berlangsung", + "stale": "Tidak dapat memperbarui sekarang", + "expired": "Status kedaluwarsa", + "signedOut": "Masuk untuk melihat agen", + "privacy": "Agen disembunyikan", + "openAgents": "Buka agen", + "running": "BERJALAN", + "needsInput": "memerlukan input", + "reconnecting": "Menghubungkan kembali", + "channelName": "Agen aktif", + "activityKitDisabledTitle": "Aktivitas Langsung nonaktif", + "activityKitDisabledBody": "Aktifkan Aktivitas Langsung di Pengaturan untuk melihat Agen Aktif di Layar Terkunci." } } diff --git a/apps/mobile/src/i18n/locales/ig.json b/apps/mobile/src/i18n/locales/ig.json index 3b160e1318..c4aadb9e50 100644 --- a/apps/mobile/src/i18n/locales/ig.json +++ b/apps/mobile/src/i18n/locales/ig.json @@ -3191,5 +3191,20 @@ "historyRetry": "Enweghị ike ibutu akụkọ nkata.", "catalogRetry": "Enweghị ike ibunye ụdị", "sendError": "Enweghị ike izipu ozi ahụ. Nwaa ọzọ." + }, + "glanceable": { + "waiting": "Na-emelite ndị ọrụ", + "empty": "Enweghị ọrụ na-aga n'ihu", + "stale": "Enweghị ike imelite ugbu a", + "expired": "Oge ọnọdụ agwụla", + "signedOut": "Banye iji hụ ndị ọrụ", + "privacy": "Ezochiri ndị ọrụ", + "openAgents": "Mepee ndị ọrụ", + "running": "NA-AGBA", + "needsInput": "chọrọ ntinye", + "reconnecting": "Na-ejikọ ọzọ", + "channelName": "Ndị ọrụ na-arụ ọrụ", + "activityKitDisabledTitle": "Agbanyụrụ Ihe Omume Dị Ndụ", + "activityKitDisabledBody": "Gbanye Ihe Omume Dị Ndụ na Ntọala iji hụ Ndị ọrụ na-arụ ọrụ na Ihuenyo Mkpọchi." } } diff --git a/apps/mobile/src/i18n/locales/is.json b/apps/mobile/src/i18n/locales/is.json index 0a8d0ab506..0988544e0d 100644 --- a/apps/mobile/src/i18n/locales/is.json +++ b/apps/mobile/src/i18n/locales/is.json @@ -3191,5 +3191,20 @@ "historyRetry": "Ekki tókst að hlaða samtalsferilinn.", "catalogRetry": "Ekki tókst að hlaða líkön", "sendError": "Ekki tókst að senda skilaboðin. Reyndu aftur." + }, + "glanceable": { + "waiting": "Uppfærir umboð", + "empty": "Engin vinna í gangi", + "stale": "Ekki hægt að uppfæra núna", + "expired": "Staða útrunnin", + "signedOut": "Skráðu þig inn til að sjá umboð", + "privacy": "Umboð falin", + "openAgents": "Opna umboð", + "running": "Í gangi", + "needsInput": "þarfnast inntaks", + "reconnecting": "Tengist aftur", + "channelName": "Virk umboð", + "activityKitDisabledTitle": "Slökkt er á Beinni virkni", + "activityKitDisabledBody": "Kveiktu á Beinni virkni í Stillingum til að sjá Virk umboð á Lásskjánum." } } diff --git a/apps/mobile/src/i18n/locales/it.json b/apps/mobile/src/i18n/locales/it.json index 003c41ead7..a6dff9624e 100644 --- a/apps/mobile/src/i18n/locales/it.json +++ b/apps/mobile/src/i18n/locales/it.json @@ -3212,5 +3212,20 @@ "historyRetry": "Impossibile caricare la cronologia della chat.", "catalogRetry": "Impossibile caricare i modelli", "sendError": "Impossibile inviare il messaggio. Riprova." + }, + "glanceable": { + "waiting": "Aggiornamento degli agenti", + "empty": "Nessun lavoro in corso", + "stale": "Impossibile aggiornare ora", + "expired": "Stato scaduto", + "signedOut": "Accedi per vedere gli agenti", + "privacy": "Agenti nascosti", + "openAgents": "Apri agenti", + "running": "IN ESECUZIONE", + "needsInput": "richiede input", + "reconnecting": "Riconnessione in corso", + "channelName": "Agenti attivi", + "activityKitDisabledTitle": "Le attività in tempo reale sono disattivate", + "activityKitDisabledBody": "Attiva le attività in tempo reale in Impostazioni per vedere gli agenti attivi sulla schermata di blocco." } } diff --git a/apps/mobile/src/i18n/locales/ja.json b/apps/mobile/src/i18n/locales/ja.json index 2c1fc47d4c..01c1057b01 100644 --- a/apps/mobile/src/i18n/locales/ja.json +++ b/apps/mobile/src/i18n/locales/ja.json @@ -3191,5 +3191,20 @@ "historyRetry": "チャット履歴を読み込めませんでした。", "catalogRetry": "モデルを読み込めませんでした", "sendError": "メッセージを送信できませんでした。もう一度お試しください。" + }, + "glanceable": { + "waiting": "エージェントを更新中", + "empty": "進行中の作業はありません", + "stale": "現在更新できません", + "expired": "ステータスの有効期限が切れました", + "signedOut": "エージェントを表示するにはサインインしてください", + "privacy": "エージェントは非表示です", + "openAgents": "エージェントを開く", + "running": "実行中", + "needsInput": "入力が必要", + "reconnecting": "再接続中", + "channelName": "アクティブなエージェント", + "activityKitDisabledTitle": "ライブアクティビティはオフです", + "activityKitDisabledBody": "ロック画面にアクティブなエージェントを表示するには、設定でライブアクティビティをオンにしてください。" } } diff --git a/apps/mobile/src/i18n/locales/ka.json b/apps/mobile/src/i18n/locales/ka.json index 7198ea9e95..fb467472e1 100644 --- a/apps/mobile/src/i18n/locales/ka.json +++ b/apps/mobile/src/i18n/locales/ka.json @@ -3191,5 +3191,20 @@ "historyRetry": "ჩატის ისტორიის ჩატვირთვა ვერ მოხერხდა.", "catalogRetry": "მოდელების ჩატვირთვა ვერ მოხერხდა", "sendError": "შეტყობინების გაგზავნა ვერ მოხერხდა. სცადეთ ხელახლა." + }, + "glanceable": { + "waiting": "აგენტების განახლება", + "empty": "მიმდინარე სამუშაო არ არის", + "stale": "ახლა განახლება ვერ ხერხდება", + "expired": "სტატუსს ვადა გაუვიდა", + "signedOut": "შედით აგენტების სანახავად", + "privacy": "აგენტები დამალულია", + "openAgents": "აგენტების გახსნა", + "running": "მუშაობს", + "needsInput": "მოითხოვს შეყვანას", + "reconnecting": "კავშირის აღდგენა", + "channelName": "აქტიური აგენტები", + "activityKitDisabledTitle": "ცოცხალი აქტივობები გამორთულია", + "activityKitDisabledBody": "ჩართეთ ცოცხალი აქტივობები პარამეტრებში, რათა დაბლოკვის ეკრანზე აქტიური აგენტები ნახოთ." } } diff --git a/apps/mobile/src/i18n/locales/kk.json b/apps/mobile/src/i18n/locales/kk.json index 53730ac5fe..f6f359ecf1 100644 --- a/apps/mobile/src/i18n/locales/kk.json +++ b/apps/mobile/src/i18n/locales/kk.json @@ -3191,5 +3191,20 @@ "historyRetry": "Чат тарихын жүктеу мүмкін болмады.", "catalogRetry": "Модельдерді жүктеу мүмкін болмады", "sendError": "Хабарламаны жіберу мүмкін болмады. Қайта көріңіз." + }, + "glanceable": { + "waiting": "Агенттер жаңартылуда", + "empty": "Орындалып жатқан жұмыс жоқ", + "stale": "Қазір жаңарту мүмкін емес", + "expired": "Күйдің мерзімі өтті", + "signedOut": "Агенттерді көру үшін кіріңіз", + "privacy": "Агенттер жасырылған", + "openAgents": "Агенттерді ашу", + "running": "Орындалуда", + "needsInput": "енгізу қажет", + "reconnecting": "Қайта қосылуда", + "channelName": "Белсенді агенттер", + "activityKitDisabledTitle": "Тікелей әрекеттер өшірулі", + "activityKitDisabledBody": "Құлыптау экранында белсенді агенттерді көру үшін Параметрлерде тікелей әрекеттерді қосыңыз." } } diff --git a/apps/mobile/src/i18n/locales/km.json b/apps/mobile/src/i18n/locales/km.json index eca66cc71b..2831e9cf43 100644 --- a/apps/mobile/src/i18n/locales/km.json +++ b/apps/mobile/src/i18n/locales/km.json @@ -3191,5 +3191,20 @@ "historyRetry": "មិនអាចផ្ទុកប្រវត្តិជជែកបានទេ។", "catalogRetry": "មិនអាចផ្ទុកគំរូបានទេ", "sendError": "មិនអាចផ្ញើសារបានទេ។ សូមព្យាយាមម្តងទៀត។" + }, + "glanceable": { + "waiting": "កំពុងធ្វើបច្ចុប្បន្នភាពភ្នាក់ងារ", + "empty": "គ្មានការងារកំពុងដំណើរការ", + "stale": "មិនអាចធ្វើបច្ចុប្បន្នភាពឥឡូវនេះបានទេ", + "expired": "ស្ថានភាពបានផុតកំណត់", + "signedOut": "ចូលដើម្បីមើលភ្នាក់ងារ", + "privacy": "ភ្នាក់ងារត្រូវបានលាក់", + "openAgents": "បើកភ្នាក់ងារ", + "running": "កំពុងដំណើរការ", + "needsInput": "ត្រូវការបញ្ចូល", + "reconnecting": "កំពុងភ្ជាប់ឡើងវិញ", + "channelName": "ភ្នាក់ងារសកម្ម", + "activityKitDisabledTitle": "សកម្មភាពបន្តផ្ទាល់ត្រូវបានបិទ", + "activityKitDisabledBody": "បើកសកម្មភាពបន្តផ្ទាល់នៅក្នុងការកំណត់ ដើម្បីមើលភ្នាក់ងារសកម្មនៅលើអេក្រង់ចាក់សោ។" } } diff --git a/apps/mobile/src/i18n/locales/kn.json b/apps/mobile/src/i18n/locales/kn.json index 3072ec1d85..c91b75e06e 100644 --- a/apps/mobile/src/i18n/locales/kn.json +++ b/apps/mobile/src/i18n/locales/kn.json @@ -3191,5 +3191,20 @@ "historyRetry": "ಚಾಟ್ ಇತಿಹಾಸವನ್ನು ಲೋಡ್ ಮಾಡಲು ಸಾಧ್ಯವಾಗಲಿಲ್ಲ.", "catalogRetry": "ಮಾದರಿಗಳನ್ನು ಲೋಡ್ ಮಾಡಲಾಗಲಿಲ್ಲ", "sendError": "ಸಂದೇಶವನ್ನು ಕಳುಹಿಸಲು ಸಾಧ್ಯವಾಗಲಿಲ್ಲ. ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ." + }, + "glanceable": { + "waiting": "ಏಜೆಂಟ್‌ಗಳನ್ನು ನವೀಕರಿಸಲಾಗುತ್ತಿದೆ", + "empty": "ಯಾವುದೇ ಕೆಲಸ ಪ್ರಗತಿಯಲ್ಲಿಲ್ಲ", + "stale": "ಈಗ ನವೀಕರಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ", + "expired": "ಸ್ಥಿತಿಯ ಅವಧಿ ಮುಗಿದಿದೆ", + "signedOut": "ಏಜೆಂಟ್‌ಗಳನ್ನು ನೋಡಲು ಸೈನ್ ಇನ್ ಮಾಡಿ", + "privacy": "ಏಜೆಂಟ್‌ಗಳನ್ನು ಮರೆಮಾಡಲಾಗಿದೆ", + "openAgents": "ಏಜೆಂಟ್‌ಗಳನ್ನು ತೆರೆಯಿರಿ", + "running": "ಚಾಲನೆಯಲ್ಲಿದೆ", + "needsInput": "ಇನ್‌ಪುಟ್ ಅಗತ್ಯವಿದೆ", + "reconnecting": "ಮರುಸಂಪರ್ಕಿಸಲಾಗುತ್ತಿದೆ", + "channelName": "ಸಕ್ರಿಯ ಏಜೆಂಟ್‌ಗಳು", + "activityKitDisabledTitle": "ನೇರ ಚಟುವಟಿಕೆಗಳು ಆಫ್ ಆಗಿವೆ", + "activityKitDisabledBody": "ಲಾಕ್ ಪರದೆಯಲ್ಲಿ ಸಕ್ರಿಯ ಏಜೆಂಟ್‌ಗಳನ್ನು ನೋಡಲು ಸೆಟ್ಟಿಂಗ್‌ಗಳಲ್ಲಿ ನೇರ ಚಟುವಟಿಕೆಗಳನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಿ." } } diff --git a/apps/mobile/src/i18n/locales/ko.json b/apps/mobile/src/i18n/locales/ko.json index e33e68497e..9af46ce32a 100644 --- a/apps/mobile/src/i18n/locales/ko.json +++ b/apps/mobile/src/i18n/locales/ko.json @@ -3191,5 +3191,20 @@ "historyRetry": "채팅 기록을 불러올 수 없습니다.", "catalogRetry": "모델을 불러올 수 없습니다", "sendError": "메시지를 보낼 수 없습니다. 다시 시도하세요." + }, + "glanceable": { + "waiting": "에이전트 업데이트 중", + "empty": "진행 중인 작업 없음", + "stale": "지금 업데이트할 수 없습니다", + "expired": "상태 만료됨", + "signedOut": "에이전트를 보려면 로그인하세요", + "privacy": "에이전트 숨겨짐", + "openAgents": "에이전트 열기", + "running": "실행 중", + "needsInput": "입력 필요", + "reconnecting": "다시 연결 중", + "channelName": "활성 에이전트", + "activityKitDisabledTitle": "실시간 현황이 꺼져 있습니다", + "activityKitDisabledBody": "잠금 화면에서 활성 에이전트를 보려면 설정에서 실시간 현황을 켜세요." } } diff --git a/apps/mobile/src/i18n/locales/lo.json b/apps/mobile/src/i18n/locales/lo.json index ab27e9f01a..0f5dc14831 100644 --- a/apps/mobile/src/i18n/locales/lo.json +++ b/apps/mobile/src/i18n/locales/lo.json @@ -3191,5 +3191,20 @@ "historyRetry": "ບໍ່ສາມາດໂຫຼດປະຫວັດການສົນທະນາໄດ້.", "catalogRetry": "ບໍ່ສາມາດໂຫຼດໂມເດວ", "sendError": "ບໍ່ສາມາດສົ່ງຂໍ້ຄວາມໄດ້. ລອງອີກຄັ້ງ." + }, + "glanceable": { + "waiting": "ກຳລັງອັບເດດຕົວແທນ", + "empty": "ບໍ່ມີວຽກທີ່ກຳລັງດຳເນີນການ", + "stale": "ບໍ່ສາມາດອັບເດດໄດ້ໃນຕອນນີ້", + "expired": "ສະຖານະໝົດອາຍຸແລ້ວ", + "signedOut": "ເຂົ້າສູ່ລະບົບເພື່ອເບິ່ງຕົວແທນ", + "privacy": "ຕົວແທນຖືກເຊື່ອງໄວ້", + "openAgents": "ເປີດຕົວແທນ", + "running": "ກຳລັງດຳເນີນການ", + "needsInput": "ຕ້ອງການຂໍ້ມູນເຂົ້າ", + "reconnecting": "ກຳລັງເຊື່ອມຕໍ່ຄືນ", + "channelName": "ຕົວແທນທີ່ກຳລັງເຮັດວຽກ", + "activityKitDisabledTitle": "ກິດຈະກຳສົດປິດຢູ່", + "activityKitDisabledBody": "ເປີດກິດຈະກຳສົດໃນການຕັ້ງຄ່າ ເພື່ອເບິ່ງຕົວແທນທີ່ກຳລັງເຮັດວຽກໃນໜ້າຈໍລັອກ." } } diff --git a/apps/mobile/src/i18n/locales/lt.json b/apps/mobile/src/i18n/locales/lt.json index e56008b2bd..00b614f2ab 100644 --- a/apps/mobile/src/i18n/locales/lt.json +++ b/apps/mobile/src/i18n/locales/lt.json @@ -3233,5 +3233,20 @@ "historyRetry": "Nepavyko įkelti pokalbio istorijos.", "catalogRetry": "Nepavyko įkelti modelių", "sendError": "Nepavyko išsiųsti žinutės. Bandykite dar kartą." + }, + "glanceable": { + "waiting": "Atnaujinami agentai", + "empty": "Nėra vykdomų darbų", + "stale": "Dabar nepavyksta atnaujinti", + "expired": "Būsenos galiojimas baigėsi", + "signedOut": "Prisijunkite, kad matytumėte agentus", + "privacy": "Agentai paslėpti", + "openAgents": "Atidaryti agentus", + "running": "Vykdoma", + "needsInput": "reikia įvesties", + "reconnecting": "Jungiamasi iš naujo", + "channelName": "Aktyvūs agentai", + "activityKitDisabledTitle": "Tiesioginės veiklos išjungtos", + "activityKitDisabledBody": "Nustatymuose įjunkite tiesiogines veiklas, kad užrakinimo ekrane matytumėte aktyvius agentus." } } diff --git a/apps/mobile/src/i18n/locales/lv.json b/apps/mobile/src/i18n/locales/lv.json index 6fd111fda3..e9da3f9f1a 100644 --- a/apps/mobile/src/i18n/locales/lv.json +++ b/apps/mobile/src/i18n/locales/lv.json @@ -3212,5 +3212,20 @@ "historyRetry": "Neizdevās ielādēt tērzēšanas vēsturi.", "catalogRetry": "Neizdevās ielādēt modeļus", "sendError": "Neizdevās nosūtīt ziņojumu. Mēģini vēlreiz." + }, + "glanceable": { + "waiting": "Atjaunina aģentus", + "empty": "Nav darba procesā", + "stale": "Pašlaik nevar atjaunināt", + "expired": "Statusa derīgums ir beidzies", + "signedOut": "Pieraksties, lai redzētu aģentus", + "privacy": "Aģenti ir paslēpti", + "openAgents": "Atvērt aģentus", + "running": "DARBOJAS", + "needsInput": "nepieciešama ievade", + "reconnecting": "Atkārtoti izveido savienojumu", + "channelName": "Aktīvie aģenti", + "activityKitDisabledTitle": "Tiešraides aktivitātes ir izslēgtas", + "activityKitDisabledBody": "Ieslēdz tiešraides aktivitātes iestatījumos, lai bloķēšanas ekrānā redzētu aktīvos aģentus." } } diff --git a/apps/mobile/src/i18n/locales/mg.json b/apps/mobile/src/i18n/locales/mg.json index ea0a2f42cb..dbb5cdcf65 100644 --- a/apps/mobile/src/i18n/locales/mg.json +++ b/apps/mobile/src/i18n/locales/mg.json @@ -3191,5 +3191,20 @@ "historyRetry": "Tsy afaka nampidirina ny tantaran'ny resaka.", "catalogRetry": "Tsy afaka namaky ny modely", "sendError": "Tsy afaka nalefa ny hafatra. Andramo indray." + }, + "glanceable": { + "waiting": "Manavao ny agent", + "empty": "Tsy misy asa mandeha", + "stale": "Tsy afaka manavao izao", + "expired": "Lany daty ny sata", + "signedOut": "Midira mba hahitana ny agent", + "privacy": "Nafenina ny agent", + "openAgents": "Sokafy ny agent", + "running": "MANDEHA", + "needsInput": "mila fampidirana", + "reconnecting": "Mampifandray indray", + "channelName": "Agent mavitrika", + "activityKitDisabledTitle": "Tsy mandeha ny Hetsika Mivantana", + "activityKitDisabledBody": "Alefaso ao amin'ny Fikirana ny Hetsika Mivantana mba hahitana ny Agent Mavitrika eo amin'ny Efijery Fihidy." } } diff --git a/apps/mobile/src/i18n/locales/mi.json b/apps/mobile/src/i18n/locales/mi.json index 64d9167f72..48614e612d 100644 --- a/apps/mobile/src/i18n/locales/mi.json +++ b/apps/mobile/src/i18n/locales/mi.json @@ -3191,5 +3191,20 @@ "historyRetry": "Kāore i taea te uta te hītori kōrero.", "catalogRetry": "Kāore i taea te uta i ngā tauira", "sendError": "I rahua te tuku karere. Whakamātau anō." + }, + "glanceable": { + "waiting": "Kei te whakahou i ngā māngai", + "empty": "Kāore he mahi e haere ana", + "stale": "Kāore e taea te whakahou ināianei", + "expired": "Kua pau te mana o te tūnga", + "signedOut": "Takiuru kia kite i ngā māngai", + "privacy": "Kua huna ngā māngai", + "openAgents": "Whakatuwheratia ngā māngai", + "running": "Kei te oma", + "needsInput": "e hiahia ana ki te whakaurunga", + "reconnecting": "Kei te hono anō", + "channelName": "Ngā māngai hohe", + "activityKitDisabledTitle": "Kua whakawetohia ngā Mahi Mataora", + "activityKitDisabledBody": "Whakakāngia ngā Mahi Mataora i Ngā tautuhinga kia kite i ngā Māngai Hohe i te Mata Maukati." } } diff --git a/apps/mobile/src/i18n/locales/mk.json b/apps/mobile/src/i18n/locales/mk.json index 8187fc07af..ba85de30a5 100644 --- a/apps/mobile/src/i18n/locales/mk.json +++ b/apps/mobile/src/i18n/locales/mk.json @@ -3191,5 +3191,20 @@ "historyRetry": "Не можев да ја вчитам историјата на разговорот.", "catalogRetry": "Не можеше да се вчитаат моделите", "sendError": "Не можеше да се испрати пораката. Обидете се повторно." + }, + "glanceable": { + "waiting": "Ажурирање на агентите", + "empty": "Нема работа во тек", + "stale": "Не може да се ажурира сега", + "expired": "Статусот истече", + "signedOut": "Најавете се за да ги видите агентите", + "privacy": "Агентите се скриени", + "openAgents": "Отворете ги агентите", + "running": "Во тек", + "needsInput": "бара внес", + "reconnecting": "Повторно поврзување", + "channelName": "Активни агенти", + "activityKitDisabledTitle": "Активностите во живо се исклучени", + "activityKitDisabledBody": "Вклучете Активности во живо во Поставки за да ги видите активните агенти на заклучениот екран." } } diff --git a/apps/mobile/src/i18n/locales/ml.json b/apps/mobile/src/i18n/locales/ml.json index 3a4edeb6b8..9322d2271d 100644 --- a/apps/mobile/src/i18n/locales/ml.json +++ b/apps/mobile/src/i18n/locales/ml.json @@ -3191,5 +3191,20 @@ "historyRetry": "ചാറ്റ് ചരിത്രം ലോഡ് ചെയ്യാനായില്ല.", "catalogRetry": "മോഡലുകൾ ലോഡ് ചെയ്യാൻ കഴിഞ്ഞില്ല", "sendError": "സന്ദേശം അയയ്ക്കാൻ പരാജയപ്പെട്ടു. വീണ്ടും ശ്രമിക്കുക." + }, + "glanceable": { + "waiting": "ഏജന്റുകളെ അപ്ഡേറ്റ് ചെയ്യുന്നു", + "empty": "ജോലികളൊന്നും പുരോഗമിക്കുന്നില്ല", + "stale": "ഇപ്പോൾ അപ്ഡേറ്റ് ചെയ്യാൻ കഴിയില്ല", + "expired": "നില കാലഹരണപ്പെട്ടു", + "signedOut": "ഏജന്റുകളെ കാണാൻ സൈൻ ഇൻ ചെയ്യുക", + "privacy": "ഏജന്റുകളെ മറച്ചിരിക്കുന്നു", + "openAgents": "ഏജന്റുകളെ തുറക്കുക", + "running": "പ്രവർത്തിക്കുന്നു", + "needsInput": "ഇൻപുട്ട് ആവശ്യമാണ്", + "reconnecting": "വീണ്ടും ബന്ധിപ്പിക്കുന്നു", + "channelName": "സജീവ ഏജന്റുകൾ", + "activityKitDisabledTitle": "തത്സമയ പ്രവർത്തനങ്ങൾ ഓഫാണ്", + "activityKitDisabledBody": "ലോക്ക് സ്ക്രീനിൽ സജീവ ഏജന്റുകളെ കാണാൻ ക്രമീകരണങ്ങളിൽ തത്സമയ പ്രവർത്തനങ്ങൾ പ്രവർത്തനക്ഷമമാക്കുക." } } diff --git a/apps/mobile/src/i18n/locales/mn.json b/apps/mobile/src/i18n/locales/mn.json index 1c54fdb0cd..b81eab872a 100644 --- a/apps/mobile/src/i18n/locales/mn.json +++ b/apps/mobile/src/i18n/locales/mn.json @@ -3191,5 +3191,20 @@ "historyRetry": "Чат түүхийг ачаалж чадсангүй.", "catalogRetry": "Загваруудыг ачаалж чадсангүй", "sendError": "Мессежийг илгээж чадсангүй. Дахин оролдоно уу." + }, + "glanceable": { + "waiting": "Агентуудыг шинэчилж байна", + "empty": "Гүйцэтгэж буй ажил алга", + "stale": "Одоо шинэчлэх боломжгүй", + "expired": "Төлөвийн хугацаа дууссан", + "signedOut": "Агентуудыг харахын тулд нэвтэрнэ үү", + "privacy": "Агентуудыг нуусан", + "openAgents": "Агентуудыг нээх", + "running": "АЖИЛЛАЖ БАЙНА", + "needsInput": "оролт шаардлагатай", + "reconnecting": "Дахин холбогдож байна", + "channelName": "Идэвхтэй агентууд", + "activityKitDisabledTitle": "Шууд үйл ажиллагаа унтраалттай байна", + "activityKitDisabledBody": "Түгжээтэй дэлгэц дээр Идэвхтэй агентуудыг харахын тулд Тохиргоо хэсэгт Шууд үйл ажиллагааг асаана уу." } } diff --git a/apps/mobile/src/i18n/locales/mr.json b/apps/mobile/src/i18n/locales/mr.json index c440e30f9f..b938634c68 100644 --- a/apps/mobile/src/i18n/locales/mr.json +++ b/apps/mobile/src/i18n/locales/mr.json @@ -3191,5 +3191,20 @@ "historyRetry": "चॅट इतिहास लोड करता आला नाही.", "catalogRetry": "मॉडेल लोड करता आले नाहीत", "sendError": "संदेश पाठवण्यात अयशस्वी. कृपया पुन्हा प्रयत्न करा." + }, + "glanceable": { + "waiting": "एजंट्स अद्यतनित करत आहे", + "empty": "कोणतेही काम सुरू नाही", + "stale": "आता अद्यतनित करता येत नाही", + "expired": "स्थिती कालबाह्य झाली", + "signedOut": "एजंट्स पाहण्यासाठी साइन इन करा", + "privacy": "एजंट्स लपवले आहेत", + "openAgents": "एजंट्स उघडा", + "running": "चालू आहे", + "needsInput": "इनपुट आवश्यक", + "reconnecting": "पुन्हा जोडत आहे", + "channelName": "सक्रिय एजंट्स", + "activityKitDisabledTitle": "थेट क्रियाकलाप बंद आहेत", + "activityKitDisabledBody": "लॉक स्क्रीनवर सक्रिय एजंट्स पाहण्यासाठी सेटिंग्जमध्ये थेट क्रियाकलाप सुरू करा." } } diff --git a/apps/mobile/src/i18n/locales/ms.json b/apps/mobile/src/i18n/locales/ms.json index 22aec75af6..f2383b0678 100644 --- a/apps/mobile/src/i18n/locales/ms.json +++ b/apps/mobile/src/i18n/locales/ms.json @@ -3191,5 +3191,20 @@ "historyRetry": "Tidak dapat memuatkan sejarah sembang.", "catalogRetry": "Tidak dapat memuatkan model", "sendError": "Mesej tidak dapat dihantar. Cuba lagi." + }, + "glanceable": { + "waiting": "Mengemas kini ejen", + "empty": "Tiada kerja sedang dijalankan", + "stale": "Tidak dapat mengemas kini sekarang", + "expired": "Status tamat tempoh", + "signedOut": "Log masuk untuk melihat ejen", + "privacy": "Ejen disembunyikan", + "openAgents": "Buka ejen", + "running": "Sedang berjalan", + "needsInput": "perlu input", + "reconnecting": "Menyambung semula", + "channelName": "Ejen aktif", + "activityKitDisabledTitle": "Aktiviti Langsung dimatikan", + "activityKitDisabledBody": "Hidupkan Aktiviti Langsung dalam Tetapan untuk melihat Ejen Aktif pada Skrin Kunci." } } diff --git a/apps/mobile/src/i18n/locales/mt.json b/apps/mobile/src/i18n/locales/mt.json index 5e8edb2ff2..9de9bda52b 100644 --- a/apps/mobile/src/i18n/locales/mt.json +++ b/apps/mobile/src/i18n/locales/mt.json @@ -3254,5 +3254,20 @@ "historyRetry": "Ma setgħux jitgħabbew l-istorja taċ-chat.", "catalogRetry": "Ma setgħux jittellgħu l-mudelli", "sendError": "Ma setax jintbagħat il-messaġġ. Erġa' pprova." + }, + "glanceable": { + "waiting": "Qed jaġġorna l-aġenti", + "empty": "L-ebda xogħol għaddej", + "stale": "Ma jistax jaġġorna bħalissa", + "expired": "L-istatus skada", + "signedOut": "Idħol biex tara l-aġenti", + "privacy": "Aġenti moħbija", + "openAgents": "Iftaħ l-aġenti", + "running": "Għaddej", + "needsInput": "jeħtieġ input", + "reconnecting": "Qed jerġa' jaqbad", + "channelName": "Aġenti attivi", + "activityKitDisabledTitle": "L-Attivitajiet Diretti huma mitfija", + "activityKitDisabledBody": "Ixgħel l-Attivitajiet Diretti fis-Settings biex tara l-Aġenti Attivi fuq l-Iskrin Imsakkar." } } diff --git a/apps/mobile/src/i18n/locales/my.json b/apps/mobile/src/i18n/locales/my.json index a17b4d7c19..0acbfd71ca 100644 --- a/apps/mobile/src/i18n/locales/my.json +++ b/apps/mobile/src/i18n/locales/my.json @@ -3191,5 +3191,20 @@ "historyRetry": "စကားဝိုင်းသမိုင်းကို တင်မရပါ။", "catalogRetry": "မော်ဒယ်များ ဖွင့်၍ မရပါ", "sendError": "မက်ဆေ့ခ်ျ ပို့ရန် မအောင်မြင်ခဲ့ပါ။ ထပ်ကြိုးစားပါ။" + }, + "glanceable": { + "waiting": "agent များကို အပ်ဒိတ်လုပ်နေသည်", + "empty": "လုပ်ဆောင်နေသော အလုပ် မရှိပါ", + "stale": "ယခု အပ်ဒိတ်လုပ်၍ မရပါ", + "expired": "အခြေအနေ သက်တမ်းကုန်သွားသည်", + "signedOut": "agent များကို ကြည့်ရန် ဝင်ပါ", + "privacy": "agent များကို ဝှက်ထားသည်", + "openAgents": "agent များကို ဖွင့်ပါ", + "running": "လည်ပတ်နေသည်", + "needsInput": "ထည့်သွင်းမှု လိုအပ်သည်", + "reconnecting": "ပြန်ချိတ်ဆက်နေသည်", + "channelName": "လုပ်ဆောင်နေသော agent များ", + "activityKitDisabledTitle": "တိုက်ရိုက်လှုပ်ရှားမှုများ ပိတ်ထားသည်", + "activityKitDisabledBody": "သော့ခတ်မျက်နှာပြင်တွင် လုပ်ဆောင်နေသော agent များကို ကြည့်ရန် ဆက်တင်များတွင် တိုက်ရိုက်လှုပ်ရှားမှုများကို ဖွင့်ပါ။" } } diff --git a/apps/mobile/src/i18n/locales/nb.json b/apps/mobile/src/i18n/locales/nb.json index 8a906acf20..a4298c5f36 100644 --- a/apps/mobile/src/i18n/locales/nb.json +++ b/apps/mobile/src/i18n/locales/nb.json @@ -3191,5 +3191,20 @@ "historyRetry": "Kunne ikke laste inn chathistorikken.", "catalogRetry": "Kunne ikke laste modeller", "sendError": "Kunne ikke sende meldingen. Prøv igjen." + }, + "glanceable": { + "waiting": "Oppdaterer agenter", + "empty": "Ingen arbeid pågår", + "stale": "Kan ikke oppdatere nå", + "expired": "Statusen er utløpt", + "signedOut": "Logg inn for å se agenter", + "privacy": "Agenter er skjult", + "openAgents": "Åpne agenter", + "running": "KJØRER", + "needsInput": "trenger innspill", + "reconnecting": "Kobler til på nytt", + "channelName": "Aktive agenter", + "activityKitDisabledTitle": "Oppdateringer i sanntid er av", + "activityKitDisabledBody": "Slå på Oppdateringer i sanntid i Innstillinger for å se Aktive agenter på låst skjerm." } } diff --git a/apps/mobile/src/i18n/locales/ne.json b/apps/mobile/src/i18n/locales/ne.json index 3835c430b4..ecf5f482fb 100644 --- a/apps/mobile/src/i18n/locales/ne.json +++ b/apps/mobile/src/i18n/locales/ne.json @@ -3191,5 +3191,20 @@ "historyRetry": "कुराकानी इतिहास लोड गर्न सकिएन।", "catalogRetry": "मोडेलहरू लोड गर्न सकिएन", "sendError": "सन्देश पठाउन सकिएन। फेरि प्रयास गर्नुहोस्।" + }, + "glanceable": { + "waiting": "एजेन्टहरू अद्यावधिक गर्दै", + "empty": "कुनै काम चलिरहेको छैन", + "stale": "अहिले अद्यावधिक गर्न सकिँदैन", + "expired": "स्थितिको म्याद सकियो", + "signedOut": "एजेन्टहरू हेर्न साइन इन गर्नुहोस्", + "privacy": "एजेन्टहरू लुकाइएका छन्", + "openAgents": "एजेन्टहरू खोल्नुहोस्", + "running": "चलिरहेको", + "needsInput": "इनपुट चाहिन्छ", + "reconnecting": "पुनः जडान गर्दै", + "channelName": "सक्रिय एजेन्टहरू", + "activityKitDisabledTitle": "प्रत्यक्ष गतिविधिहरू बन्द छन्", + "activityKitDisabledBody": "लक स्क्रिनमा सक्रिय एजेन्टहरू हेर्न सेटिङ्समा प्रत्यक्ष गतिविधिहरू चालू गर्नुहोस्।" } } diff --git a/apps/mobile/src/i18n/locales/nl.json b/apps/mobile/src/i18n/locales/nl.json index 98ed996491..11b04a90b7 100644 --- a/apps/mobile/src/i18n/locales/nl.json +++ b/apps/mobile/src/i18n/locales/nl.json @@ -3191,5 +3191,20 @@ "historyRetry": "Kon de chatgeschiedenis niet laden.", "catalogRetry": "Kon modellen niet laden", "sendError": "Kan het bericht niet verzenden. Probeer het opnieuw." + }, + "glanceable": { + "waiting": "Agents bijwerken", + "empty": "Geen werk in uitvoering", + "stale": "Kan nu niet bijwerken", + "expired": "Status verlopen", + "signedOut": "Log in om agents te zien", + "privacy": "Agents verborgen", + "openAgents": "Agents openen", + "running": "Bezig", + "needsInput": "heeft invoer nodig", + "reconnecting": "Opnieuw verbinden", + "channelName": "Actieve agents", + "activityKitDisabledTitle": "Liveactiviteiten staan uit", + "activityKitDisabledBody": "Schakel liveactiviteiten in via Instellingen om actieve agents op het toegangsscherm te zien." } } diff --git a/apps/mobile/src/i18n/locales/om.json b/apps/mobile/src/i18n/locales/om.json index b5a61cafe8..7e7c873d39 100644 --- a/apps/mobile/src/i18n/locales/om.json +++ b/apps/mobile/src/i18n/locales/om.json @@ -3191,5 +3191,20 @@ "historyRetry": "Seenaa chaat fudhachuu hin dandeenye.", "catalogRetry": "Modeeloota fe'achuu hin dandeenye", "sendError": "Ergaa erguu hin dandeenye. Ammas yaali." + }, + "glanceable": { + "waiting": "Eejentoota haaromsaa jira", + "empty": "Hojiin hojjetamaa jiru hin jiru", + "stale": "Amma haaromsuu hin danda'u", + "expired": "Yeroon haalaa darbeera", + "signedOut": "Eejentoota arguuf seenaa", + "privacy": "Eejentoonni dhokamaniiru", + "openAgents": "Eejentoota banaa", + "running": "Hojii irra jira", + "needsInput": "seensa barbaada", + "reconnecting": "Irra deebi'ee walqabachaa jira", + "channelName": "Eejentoota hojii irra jiran", + "activityKitDisabledTitle": "Sochiiwwan Kallattii cufamaniiru", + "activityKitDisabledBody": "Eejentoota hojii irra jiran Iskiriinii Qulfii irratti arguuf, Qindaa'ina keessatti Sochiiwwan Kallattii banaa." } } diff --git a/apps/mobile/src/i18n/locales/or.json b/apps/mobile/src/i18n/locales/or.json index 62fdd2d1cd..28dfd8f459 100644 --- a/apps/mobile/src/i18n/locales/or.json +++ b/apps/mobile/src/i18n/locales/or.json @@ -3191,5 +3191,20 @@ "historyRetry": "ଚାଟ୍ ଇତିହାସ ଲୋଡ୍ ହୋଇପାରିଲା ନାହିଁ।", "catalogRetry": "ମଡେଲ ଲୋଡ୍ କରାଯାଇପାରିଲା ନାହିଁ", "sendError": "ସନ୍ଦେଶ ପଠାଯାଇପାରିଲା ନାହିଁ। ପୁଣି ଚେଷ୍ଟା କରନ୍ତୁ।" + }, + "glanceable": { + "waiting": "ଏଜେଣ୍ଟଗୁଡ଼ିକ ଅପଡେଟ୍ ହେଉଛି", + "empty": "କୌଣସି କାର୍ଯ୍ୟ ଚାଲୁ ନାହିଁ", + "stale": "ଏବେ ଅପଡେଟ୍ କରିହେବ ନାହିଁ", + "expired": "ସ୍ଥିତିର ଅବଧି ସମାପ୍ତ ହୋଇଛି", + "signedOut": "ଏଜେଣ୍ଟଗୁଡ଼ିକ ଦେଖିବାକୁ ସାଇନ୍ ଇନ୍ କରନ୍ତୁ", + "privacy": "ଏଜେଣ୍ଟଗୁଡ଼ିକ ଲୁଚାଯାଇଛି", + "openAgents": "ଏଜେଣ୍ଟଗୁଡ଼ିକ ଖୋଲନ୍ତୁ", + "running": "ଚାଲୁଛି", + "needsInput": "ଇନପୁଟ୍ ଆବଶ୍ୟକ", + "reconnecting": "ପୁଣି ସଂଯୋଗ ହେଉଛି", + "channelName": "ସକ୍ରିୟ ଏଜେଣ୍ଟଗୁଡ଼ିକ", + "activityKitDisabledTitle": "ଲାଇଭ୍ କାର୍ଯ୍ୟକଳାପ ବନ୍ଦ ଅଛି", + "activityKitDisabledBody": "ଲକ୍ ସ୍କ୍ରିନ୍‌ରେ ସକ୍ରିୟ ଏଜେଣ୍ଟଗୁଡ଼ିକ ଦେଖିବାକୁ ସେଟିଂସ୍‌ରେ ଲାଇଭ୍ କାର୍ଯ୍ୟକଳାପ ଚାଲୁ କରନ୍ତୁ।" } } diff --git a/apps/mobile/src/i18n/locales/pa.json b/apps/mobile/src/i18n/locales/pa.json index a17c3d9df7..dc384c98b0 100644 --- a/apps/mobile/src/i18n/locales/pa.json +++ b/apps/mobile/src/i18n/locales/pa.json @@ -3191,5 +3191,20 @@ "historyRetry": "ਚੈਟ ਇਤਿਹਾਸ ਲੋਡ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਿਆ।", "catalogRetry": "ਮਾਡਲ ਲੋਡ ਨਹੀਂ ਕੀਤੇ ਜਾ ਸਕੇ", "sendError": "ਸੁਨੇਹਾ ਭੇਜਿਆ ਨਹੀਂ ਜਾ ਸਕਿਆ। ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ।" + }, + "glanceable": { + "waiting": "ਏਜੰਟ ਅੱਪਡੇਟ ਕੀਤੇ ਜਾ ਰਹੇ ਹਨ", + "empty": "ਕੋਈ ਕੰਮ ਜਾਰੀ ਨਹੀਂ ਹੈ", + "stale": "ਹੁਣ ਅੱਪਡੇਟ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਦਾ", + "expired": "ਸਥਿਤੀ ਦੀ ਮਿਆਦ ਪੁੱਗ ਗਈ ਹੈ", + "signedOut": "ਏਜੰਟ ਦੇਖਣ ਲਈ ਸਾਈਨ ਇਨ ਕਰੋ", + "privacy": "ਏਜੰਟ ਲੁਕਾਏ ਗਏ ਹਨ", + "openAgents": "ਏਜੰਟ ਖੋਲ੍ਹੋ", + "running": "ਚੱਲ ਰਿਹਾ ਹੈ", + "needsInput": "ਇਨਪੁੱਟ ਦੀ ਲੋੜ ਹੈ", + "reconnecting": "ਮੁੜ ਕਨੈਕਟ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ", + "channelName": "ਸਰਗਰਮ ਏਜੰਟ", + "activityKitDisabledTitle": "ਲਾਈਵ ਗਤੀਵਿਧੀਆਂ ਬੰਦ ਹਨ", + "activityKitDisabledBody": "ਲਾਕ ਸਕ੍ਰੀਨ 'ਤੇ ਸਰਗਰਮ ਏਜੰਟ ਦੇਖਣ ਲਈ ਸੈਟਿੰਗਾਂ ਵਿੱਚ ਲਾਈਵ ਗਤੀਵਿਧੀਆਂ ਚਾਲੂ ਕਰੋ।" } } diff --git a/apps/mobile/src/i18n/locales/pl.json b/apps/mobile/src/i18n/locales/pl.json index e57878bb6b..e0b439e9c0 100644 --- a/apps/mobile/src/i18n/locales/pl.json +++ b/apps/mobile/src/i18n/locales/pl.json @@ -3233,5 +3233,20 @@ "historyRetry": "Nie udało się wczytać historii czatu.", "catalogRetry": "Nie można załadować modeli", "sendError": "Nie udało się wysłać wiadomości. Spróbuj ponownie." + }, + "glanceable": { + "waiting": "Aktualizowanie agentów", + "empty": "Brak pracy w toku", + "stale": "Nie można teraz zaktualizować", + "expired": "Status wygasł", + "signedOut": "Zaloguj się, aby zobaczyć agentów", + "privacy": "Agenci ukryci", + "openAgents": "Otwórz agentów", + "running": "W toku", + "needsInput": "wymaga danych", + "reconnecting": "Ponowne łączenie", + "channelName": "Aktywni agenci", + "activityKitDisabledTitle": "Wydarzenia na żywo są wyłączone", + "activityKitDisabledBody": "Włącz wydarzenia na żywo w Ustawieniach, aby widzieć aktywnych agentów na ekranie blokady." } } diff --git a/apps/mobile/src/i18n/locales/ps.json b/apps/mobile/src/i18n/locales/ps.json index aac55b353f..023a93365b 100644 --- a/apps/mobile/src/i18n/locales/ps.json +++ b/apps/mobile/src/i18n/locales/ps.json @@ -3191,5 +3191,20 @@ "historyRetry": "د چټ تاریخ بار کیدلای نه شو.", "catalogRetry": "ماډلونه بار نه شول", "sendError": "د پیغام لیږل ممکن نه شول. بیا هڅه وکړئ." + }, + "glanceable": { + "waiting": "اجنټان تازه کېږي", + "empty": "هیڅ کار روان نه دی", + "stale": "اوس تازه کول ناشوني دي", + "expired": "د حالت اعتبار پای ته رسېدلی", + "signedOut": "د اجنټانو د لیدلو لپاره ننوزئ", + "privacy": "اجنټان پټ دي", + "openAgents": "اجنټان پرانیزئ", + "running": "روان", + "needsInput": "ورودی ته اړتیا لري", + "reconnecting": "بیا نښلېږي", + "channelName": "فعال اجنټان", + "activityKitDisabledTitle": "ژوندي فعالیتونه بند دي", + "activityKitDisabledBody": "په قلف شوې پرده کې د فعالو اجنټانو د لیدلو لپاره په ترتیباتو کې ژوندي فعالیتونه فعال کړئ." } } diff --git a/apps/mobile/src/i18n/locales/pt-BR.json b/apps/mobile/src/i18n/locales/pt-BR.json index 380b1cfab5..ce93386fce 100644 --- a/apps/mobile/src/i18n/locales/pt-BR.json +++ b/apps/mobile/src/i18n/locales/pt-BR.json @@ -3212,5 +3212,20 @@ "historyRetry": "Não foi possível carregar o histórico do chat.", "catalogRetry": "Não foi possível carregar os modelos", "sendError": "Não foi possível enviar a mensagem. Tente novamente." + }, + "glanceable": { + "waiting": "Atualizando agentes", + "empty": "Nenhum trabalho em andamento", + "stale": "Não é possível atualizar agora", + "expired": "Status expirado", + "signedOut": "Entre para ver os agentes", + "privacy": "Agentes ocultos", + "openAgents": "Abrir agentes", + "running": "Em execução", + "needsInput": "requer entrada", + "reconnecting": "Reconectando", + "channelName": "Agentes ativos", + "activityKitDisabledTitle": "As Atividades ao Vivo estão desativadas", + "activityKitDisabledBody": "Ative as Atividades ao Vivo em Ajustes para ver os agentes ativos na Tela Bloqueada." } } diff --git a/apps/mobile/src/i18n/locales/pt.json b/apps/mobile/src/i18n/locales/pt.json index 88fbcf071d..73f9fcdd8c 100644 --- a/apps/mobile/src/i18n/locales/pt.json +++ b/apps/mobile/src/i18n/locales/pt.json @@ -3212,5 +3212,20 @@ "historyRetry": "Não foi possível carregar o histórico do chat.", "catalogRetry": "Não foi possível carregar os modelos", "sendError": "Não foi possível enviar a mensagem. Tente novamente." + }, + "glanceable": { + "waiting": "A atualizar agentes", + "empty": "Nenhum trabalho em curso", + "stale": "Não é possível atualizar agora", + "expired": "Estado expirado", + "signedOut": "Inicie sessão para ver os agentes", + "privacy": "Agentes ocultos", + "openAgents": "Abrir agentes", + "running": "EM EXECUÇÃO", + "needsInput": "requer entrada", + "reconnecting": "A restabelecer ligação", + "channelName": "Agentes ativos", + "activityKitDisabledTitle": "As Atividades em tempo real estão desativadas", + "activityKitDisabledBody": "Ative as Atividades em tempo real nas Definições para ver os agentes ativos no Ecrã bloqueado." } } diff --git a/apps/mobile/src/i18n/locales/ro.json b/apps/mobile/src/i18n/locales/ro.json index c38ab7a101..f1a3aa9777 100644 --- a/apps/mobile/src/i18n/locales/ro.json +++ b/apps/mobile/src/i18n/locales/ro.json @@ -3212,5 +3212,20 @@ "historyRetry": "Nu s-a putut încărca istoricul de chat.", "catalogRetry": "Modelele nu au putut fi încărcate", "sendError": "Mesajul nu a putut fi trimis. Încercați din nou." + }, + "glanceable": { + "waiting": "Se actualizează agenții", + "empty": "Nicio sarcină în curs", + "stale": "Nu se poate actualiza acum", + "expired": "Stare expirată", + "signedOut": "Autentifică-te pentru a vedea agenții", + "privacy": "Agenți ascunși", + "openAgents": "Deschide agenții", + "running": "Rulează", + "needsInput": "necesită introducere", + "reconnecting": "Se reconectează", + "channelName": "Agenți activi", + "activityKitDisabledTitle": "Activitățile live sunt dezactivate", + "activityKitDisabledBody": "Activează Activități live în Setări pentru a vedea Agenții activi pe ecranul de blocare." } } diff --git a/apps/mobile/src/i18n/locales/ru.json b/apps/mobile/src/i18n/locales/ru.json index 49dc2e426f..8412bf2ac9 100644 --- a/apps/mobile/src/i18n/locales/ru.json +++ b/apps/mobile/src/i18n/locales/ru.json @@ -3233,5 +3233,20 @@ "historyRetry": "Не удалось загрузить историю чата.", "catalogRetry": "Не удалось загрузить модели", "sendError": "Не удалось отправить сообщение. Попробуйте еще раз." + }, + "glanceable": { + "waiting": "Обновление агентов", + "empty": "Нет задач в работе", + "stale": "Сейчас не удается обновить", + "expired": "Статус устарел", + "signedOut": "Войдите, чтобы видеть агентов", + "privacy": "Агенты скрыты", + "openAgents": "Открыть агентов", + "running": "Выполняется", + "needsInput": "требует ввода", + "reconnecting": "Повторное подключение", + "channelName": "Активные агенты", + "activityKitDisabledTitle": "Эфир активности выключен", + "activityKitDisabledBody": "Включите Эфир активности в Настройках, чтобы видеть активных агентов на экране блокировки." } } diff --git a/apps/mobile/src/i18n/locales/si.json b/apps/mobile/src/i18n/locales/si.json index 7bfd45e2e1..f8bd34b3e0 100644 --- a/apps/mobile/src/i18n/locales/si.json +++ b/apps/mobile/src/i18n/locales/si.json @@ -3191,5 +3191,20 @@ "historyRetry": "සංවාද ඉතිහාසය පූරණය කළ නොහැකි විය.", "catalogRetry": "ආකෘති පූරණය කිරීමට නොහැකි විය", "sendError": "පණිවිඩය යැවිය නොහැකි විය. නැවත උත්සාහ කරන්න." + }, + "glanceable": { + "waiting": "නියෝජිතයන් යාවත්කාලීන කරමින්", + "empty": "සිදු කෙරෙන වැඩ කිසිවක් නැත", + "stale": "දැන් යාවත්කාලීන කළ නොහැක", + "expired": "තත්ත්වය කල් ඉකුත් වී ඇත", + "signedOut": "නියෝජිතයන් බැලීමට පුරනය වන්න", + "privacy": "නියෝජිතයන් සඟවා ඇත", + "openAgents": "නියෝජිතයන් විවෘත කරන්න", + "running": "ධාවනය වෙමින්", + "needsInput": "ආදානය අවශ්යයි", + "reconnecting": "නැවත සම්බන්ධ වෙමින්", + "channelName": "සක්‍රිය නියෝජිතයන්", + "activityKitDisabledTitle": "සජීවී ක්‍රියාකාරකම් අක්‍රියයි", + "activityKitDisabledBody": "අගුළු තිරයේ සක්‍රිය නියෝජිතයන් බැලීමට සැකසුම් තුළ සජීවී ක්‍රියාකාරකම් සක්‍රිය කරන්න." } } diff --git a/apps/mobile/src/i18n/locales/sk.json b/apps/mobile/src/i18n/locales/sk.json index ab933af808..5e66e53b27 100644 --- a/apps/mobile/src/i18n/locales/sk.json +++ b/apps/mobile/src/i18n/locales/sk.json @@ -3233,5 +3233,20 @@ "historyRetry": "Históriu chatu sa nepodarilo načítať.", "catalogRetry": "Modely sa nepodarilo načítať", "sendError": "Nepodarilo sa odoslať správu. Skúste to znova." + }, + "glanceable": { + "waiting": "Aktualizácia agentov", + "empty": "Žiadna prebiehajúca práca", + "stale": "Teraz sa nedá aktualizovať", + "expired": "Platnosť stavu vypršala", + "signedOut": "Prihláste sa na zobrazenie agentov", + "privacy": "Agenti sú skrytí", + "openAgents": "Otvoriť agentov", + "running": "Prebieha", + "needsInput": "vyžaduje vstup", + "reconnecting": "Opätovné pripájanie", + "channelName": "Aktívni agenti", + "activityKitDisabledTitle": "Živé aktivity sú vypnuté", + "activityKitDisabledBody": "Zapnite živé aktivity v Nastaveniach, aby sa aktívni agenti zobrazovali na zamknutej obrazovke." } } diff --git a/apps/mobile/src/i18n/locales/sl.json b/apps/mobile/src/i18n/locales/sl.json index 46efd54d56..4cc7c05f56 100644 --- a/apps/mobile/src/i18n/locales/sl.json +++ b/apps/mobile/src/i18n/locales/sl.json @@ -3233,5 +3233,20 @@ "historyRetry": "Zgodovine klepeta ni bilo mogoče naložiti.", "catalogRetry": "Nalaganje modelov ni uspelo", "sendError": "Sporočila ni bilo mogoče poslati. Poskusite znova." + }, + "glanceable": { + "waiting": "Posodabljanje agentov", + "empty": "Ni dela v teku", + "stale": "Trenutno ni mogoče posodobiti", + "expired": "Stanje je poteklo", + "signedOut": "Prijavite se za ogled agentov", + "privacy": "Agenti so skriti", + "openAgents": "Odprite agente", + "running": "DELUJE", + "needsInput": "potrebuje vnos", + "reconnecting": "Ponovno povezovanje", + "channelName": "Aktivni agenti", + "activityKitDisabledTitle": "Dejavnosti v živo so izklopljene", + "activityKitDisabledBody": "V Nastavitvah vklopite Dejavnosti v živo, da bodo Aktivni agenti prikazani na zaklenjenem zaslonu." } } diff --git a/apps/mobile/src/i18n/locales/so.json b/apps/mobile/src/i18n/locales/so.json index 26f277e3a5..218014c494 100644 --- a/apps/mobile/src/i18n/locales/so.json +++ b/apps/mobile/src/i18n/locales/so.json @@ -3191,5 +3191,20 @@ "historyRetry": "Taariikhda wada sheekaysiga lama soo gelin karin.", "catalogRetry": "Moodeelada lama soo dejin karin", "sendError": "Fariinta lama diri karin. Mar kale isku day." + }, + "glanceable": { + "waiting": "Cusboonaysiinaya wakiillada", + "empty": "Ma jirto hawl socota", + "stale": "Hadda lama cusboonaysiin karo", + "expired": "Xaaladdu way dhacday", + "signedOut": "Soo gal si aad u aragto wakiillada", + "privacy": "Wakiillada waa la qariyay", + "openAgents": "Fur wakiillada", + "running": "Socodaya", + "needsInput": "u baahan wax-soo-gal", + "reconnecting": "Dib u xiriirinaya", + "channelName": "Wakiillada firfircoon", + "activityKitDisabledTitle": "Hawlaha Tooska ah waa daman", + "activityKitDisabledBody": "Ku daar Hawlaha Tooska ah Dejinta si aad Wakiillada firfircoon ugu aragto Shaashadda Qufulka." } } diff --git a/apps/mobile/src/i18n/locales/sq.json b/apps/mobile/src/i18n/locales/sq.json index 23ef6f0047..dacf2759e9 100644 --- a/apps/mobile/src/i18n/locales/sq.json +++ b/apps/mobile/src/i18n/locales/sq.json @@ -3191,5 +3191,20 @@ "historyRetry": "Nuk u arrit të ngarkohej historia e bisedës.", "catalogRetry": "Nuk u ngarkuan modelet", "sendError": "Mesazhi nuk u dërgua. Provoni përsëri." + }, + "glanceable": { + "waiting": "Duke përditësuar agjentët", + "empty": "Nuk ka punë në vazhdim", + "stale": "Nuk mund të përditësohet tani", + "expired": "Statusi ka skaduar", + "signedOut": "Identifikohuni për të parë agjentët", + "privacy": "Agjentët janë fshehur", + "openAgents": "Hapni agjentët", + "running": "Në ekzekutim", + "needsInput": "ka nevojë për të dhëna", + "reconnecting": "Duke u rilidhur", + "channelName": "Agjentët aktivë", + "activityKitDisabledTitle": "Aktivitetet e drejtpërdrejta janë çaktivizuar", + "activityKitDisabledBody": "Aktivizoni Aktivitetet e drejtpërdrejta te Cilësimet për të parë Agjentët aktivë në Ekranin e kyçjes." } } diff --git a/apps/mobile/src/i18n/locales/sr.json b/apps/mobile/src/i18n/locales/sr.json index e3224a15c9..472e29f72b 100644 --- a/apps/mobile/src/i18n/locales/sr.json +++ b/apps/mobile/src/i18n/locales/sr.json @@ -3212,5 +3212,20 @@ "historyRetry": "Nije moguće učitati istoriju ćaskanja.", "catalogRetry": "Nije moguće učitati modele", "sendError": "Slanje poruke nije uspelo. Pokušajte ponovo." + }, + "glanceable": { + "waiting": "Ažuriranje agenata", + "empty": "Nema rada u toku", + "stale": "Ažuriranje trenutno nije moguće", + "expired": "Status je istekao", + "signedOut": "Prijavite se da biste videli agente", + "privacy": "Agenti su skriveni", + "openAgents": "Otvorite agente", + "running": "U toku", + "needsInput": "zahteva unos", + "reconnecting": "Ponovno povezivanje", + "channelName": "Aktivni agenti", + "activityKitDisabledTitle": "Aktivnosti uživo su isključene", + "activityKitDisabledBody": "Uključite Aktivnosti uživo u Podešavanjima da biste videli aktivne agente na zaključanom ekranu." } } diff --git a/apps/mobile/src/i18n/locales/sv.json b/apps/mobile/src/i18n/locales/sv.json index 6b2001a6ee..5ddf047e59 100644 --- a/apps/mobile/src/i18n/locales/sv.json +++ b/apps/mobile/src/i18n/locales/sv.json @@ -3191,5 +3191,20 @@ "historyRetry": "Det gick inte att läsa in chatthistoriken.", "catalogRetry": "Kunde inte läsa in modeller", "sendError": "Kunde inte skicka meddelandet. Försök igen." + }, + "glanceable": { + "waiting": "Uppdaterar agenter", + "empty": "Inget arbete pågår", + "stale": "Kan inte uppdatera nu", + "expired": "Statusen har gått ut", + "signedOut": "Logga in för att se agenter", + "privacy": "Agenter dolda", + "openAgents": "Öppna agenter", + "running": "KÖRS", + "needsInput": "kräver indata", + "reconnecting": "Återansluter", + "channelName": "Aktiva agenter", + "activityKitDisabledTitle": "Liveaktiviteter är avstängda", + "activityKitDisabledBody": "Aktivera liveaktiviteter i Inställningar för att se Aktiva agenter på låsskärmen." } } diff --git a/apps/mobile/src/i18n/locales/sw.json b/apps/mobile/src/i18n/locales/sw.json index d8f50f9715..4b57985a59 100644 --- a/apps/mobile/src/i18n/locales/sw.json +++ b/apps/mobile/src/i18n/locales/sw.json @@ -3191,5 +3191,20 @@ "historyRetry": "Haikuweza kupakia historia ya mazungumzo.", "catalogRetry": "Imeshindwa kupakia miundo", "sendError": "Haikuweza kutuma ujumbe. Jaribu tena." + }, + "glanceable": { + "waiting": "Inasasisha mawakala", + "empty": "Hakuna kazi inayoendelea", + "stale": "Haiwezi kusasisha sasa", + "expired": "Muda wa hali umeisha", + "signedOut": "Ingia ili uone mawakala", + "privacy": "Mawakala wamefichwa", + "openAgents": "Fungua mawakala", + "running": "Inaendelea", + "needsInput": "inahitaji mchango", + "reconnecting": "Inaunganisha tena", + "channelName": "Mawakala wanaofanya kazi", + "activityKitDisabledTitle": "Shughuli za Moja kwa Moja zimezimwa", + "activityKitDisabledBody": "Washa Shughuli za Moja kwa Moja katika Mipangilio ili uone Mawakala wanaofanya kazi kwenye Skrini Iliyofungwa." } } diff --git a/apps/mobile/src/i18n/locales/ta.json b/apps/mobile/src/i18n/locales/ta.json index d7285a2073..71c0233906 100644 --- a/apps/mobile/src/i18n/locales/ta.json +++ b/apps/mobile/src/i18n/locales/ta.json @@ -3191,5 +3191,20 @@ "historyRetry": "அரட்டை வரலாற்றை ஏற்ற முடியவில்லை.", "catalogRetry": "மாதிரிகளை ஏற்ற முடியவில்லை", "sendError": "செய்தியை அனுப்ப முடியவில்லை. மீண்டும் முயற்சிக்கவும்." + }, + "glanceable": { + "waiting": "முகவர்களைப் புதுப்பிக்கிறது", + "empty": "எந்தப் பணியும் நடைபெறவில்லை", + "stale": "இப்போது புதுப்பிக்க முடியவில்லை", + "expired": "நிலை காலாவதியானது", + "signedOut": "முகவர்களைக் காண உள்நுழையவும்", + "privacy": "முகவர்கள் மறைக்கப்பட்டுள்ளனர்", + "openAgents": "முகவர்களைத் திறக்கவும்", + "running": "இயங்குகிறது", + "needsInput": "உள்ளீடு தேவை", + "reconnecting": "மீண்டும் இணைக்கிறது", + "channelName": "செயலில் உள்ள முகவர்கள்", + "activityKitDisabledTitle": "நேரலைச் செயல்பாடுகள் முடக்கப்பட்டுள்ளன", + "activityKitDisabledBody": "பூட்டுத் திரையில் செயலில் உள்ள முகவர்களைக் காண அமைப்புகளில் நேரலைச் செயல்பாடுகளை இயக்கவும்." } } diff --git a/apps/mobile/src/i18n/locales/te.json b/apps/mobile/src/i18n/locales/te.json index 55972192cd..7c83980d8f 100644 --- a/apps/mobile/src/i18n/locales/te.json +++ b/apps/mobile/src/i18n/locales/te.json @@ -3191,5 +3191,20 @@ "historyRetry": "చాట్ చరిత్రను లోడ్ చేయలేకపోయాము.", "catalogRetry": "మోడళ్లను లోడ్ చేయలేకపోయాము", "sendError": "సందేశం పంపలేకపోయాము. మళ్ళీ ప్రయత్నించండి." + }, + "glanceable": { + "waiting": "ఏజెంట్లను నవీకరిస్తోంది", + "empty": "పని ఏదీ కొనసాగడం లేదు", + "stale": "ఇప్పుడు నవీకరించలేము", + "expired": "స్థితి గడువు ముగిసింది", + "signedOut": "ఏజెంట్లను చూడటానికి సైన్ ఇన్ చేయండి", + "privacy": "ఏజెంట్లు దాచబడ్డారు", + "openAgents": "ఏజెంట్లను తెరవండి", + "running": "నడుస్తోంది", + "needsInput": "ఇన్పుట్ అవసరం", + "reconnecting": "మళ్లీ కనెక్ట్ అవుతోంది", + "channelName": "చురుకైన ఏజెంట్లు", + "activityKitDisabledTitle": "ప్రత్యక్ష కార్యకలాపాలు ఆఫ్‌లో ఉన్నాయి", + "activityKitDisabledBody": "లాక్ స్క్రీన్‌పై చురుకైన ఏజెంట్లను చూడటానికి సెట్టింగ్‌లలో ప్రత్యక్ష కార్యకలాపాలను ఆన్ చేయండి." } } diff --git a/apps/mobile/src/i18n/locales/th.json b/apps/mobile/src/i18n/locales/th.json index cc1e21766f..b1cad084ff 100644 --- a/apps/mobile/src/i18n/locales/th.json +++ b/apps/mobile/src/i18n/locales/th.json @@ -3191,5 +3191,20 @@ "historyRetry": "ไม่สามารถโหลดประวัติแชทได้", "catalogRetry": "ไม่สามารถโหลดโมเดลได้", "sendError": "ไม่สามารถส่งข้อความได้ ลองอีกครั้ง" + }, + "glanceable": { + "waiting": "กำลังอัปเดตเอเจนต์", + "empty": "ไม่มีงานที่กำลังดำเนินการ", + "stale": "ไม่สามารถอัปเดตได้ในขณะนี้", + "expired": "สถานะหมดอายุ", + "signedOut": "ลงชื่อเข้าใช้เพื่อดูเอเจนต์", + "privacy": "ซ่อนเอเจนต์อยู่", + "openAgents": "เปิดเอเจนต์", + "running": "กำลังทำงาน", + "needsInput": "ต้องป้อนข้อมูล", + "reconnecting": "กำลังเชื่อมต่อใหม่", + "channelName": "เอเจนต์ที่กำลังทำงาน", + "activityKitDisabledTitle": "กิจกรรมสดปิดอยู่", + "activityKitDisabledBody": "เปิดกิจกรรมสดในการตั้งค่าเพื่อดูเอเจนต์ที่กำลังทำงานบนหน้าจอล็อค" } } diff --git a/apps/mobile/src/i18n/locales/tr.json b/apps/mobile/src/i18n/locales/tr.json index e25c538ed5..ea85addb9b 100644 --- a/apps/mobile/src/i18n/locales/tr.json +++ b/apps/mobile/src/i18n/locales/tr.json @@ -3191,5 +3191,20 @@ "historyRetry": "Sohbet geçmişi yüklenemedi.", "catalogRetry": "Modeller yüklenemedi", "sendError": "Mesaj gönderilemedi. Lütfen tekrar deneyin." + }, + "glanceable": { + "waiting": "Ajanlar güncelleniyor", + "empty": "Devam eden iş yok", + "stale": "Şu anda güncellenemiyor", + "expired": "Durumun süresi doldu", + "signedOut": "Ajanları görmek için oturum açın", + "privacy": "Ajanlar gizli", + "openAgents": "Ajanları açın", + "running": "Çalışıyor", + "needsInput": "Girdi gerekli", + "reconnecting": "Yeniden bağlanılıyor", + "channelName": "Etkin ajanlar", + "activityKitDisabledTitle": "Canlı Etkinlikler kapalı", + "activityKitDisabledBody": "Etkin ajanları Kilit Ekranı'nda görmek için Ayarlar'dan Canlı Etkinlikler'i açın." } } diff --git a/apps/mobile/src/i18n/locales/uk.json b/apps/mobile/src/i18n/locales/uk.json index 0cbe343555..3a959178d8 100644 --- a/apps/mobile/src/i18n/locales/uk.json +++ b/apps/mobile/src/i18n/locales/uk.json @@ -3233,5 +3233,20 @@ "historyRetry": "Не вдалося завантажити історію чату.", "catalogRetry": "Не вдалося завантажити моделі", "sendError": "Не вдалося надіслати повідомлення. Спробуйте ще раз." + }, + "glanceable": { + "waiting": "Оновлення агентів", + "empty": "Немає поточної роботи", + "stale": "Зараз не вдається оновити", + "expired": "Термін дії статусу минув", + "signedOut": "Увійдіть, щоб бачити агентів", + "privacy": "Агентів приховано", + "openAgents": "Відкрити агентів", + "running": "Виконується", + "needsInput": "потребує вводу", + "reconnecting": "Повторне підключення", + "channelName": "Активні агенти", + "activityKitDisabledTitle": "Дії наживо вимкнено", + "activityKitDisabledBody": "Увімкніть «Дії наживо» в «Параметрах», щоб бачити активних агентів на замкненому екрані." } } diff --git a/apps/mobile/src/i18n/locales/ur.json b/apps/mobile/src/i18n/locales/ur.json index 5fa3486513..7acf73692f 100644 --- a/apps/mobile/src/i18n/locales/ur.json +++ b/apps/mobile/src/i18n/locales/ur.json @@ -3191,5 +3191,20 @@ "historyRetry": "چیٹ کی سرگزشت لوڈ نہیں ہو سکی۔", "catalogRetry": "ماڈلز لوڈ نہیں ہو سکے", "sendError": "پیغام نہیں بھیجا جا سکا۔ دوبارہ کوشش کریں۔" + }, + "glanceable": { + "waiting": "ایجنٹس اپڈیٹ ہو رہے ہیں", + "empty": "کوئی کام جاری نہیں", + "stale": "ابھی اپڈیٹ نہیں ہو سکتا", + "expired": "حالت کی میعاد ختم ہو گئی", + "signedOut": "ایجنٹس دیکھنے کے لیے سائن ان کریں", + "privacy": "ایجنٹس چھپے ہوئے ہیں", + "openAgents": "ایجنٹس کھولیں", + "running": "چل رہا ہے", + "needsInput": "ان پٹ درکار", + "reconnecting": "دوبارہ منسلک ہو رہا ہے", + "channelName": "فعال ایجنٹس", + "activityKitDisabledTitle": "لائیو سرگرمیاں بند ہیں", + "activityKitDisabledBody": "لاک اسکرین پر فعال ایجنٹس دیکھنے کے لیے ترتیبات میں لائیو سرگرمیاں فعال کریں۔" } } diff --git a/apps/mobile/src/i18n/locales/uz.json b/apps/mobile/src/i18n/locales/uz.json index a4b17d1560..550efd65b8 100644 --- a/apps/mobile/src/i18n/locales/uz.json +++ b/apps/mobile/src/i18n/locales/uz.json @@ -3191,5 +3191,20 @@ "historyRetry": "Suhbat tarixini yuklab bo'lmadi.", "catalogRetry": "Modellarni yuklab bo'lmadi", "sendError": "Xabarni yuborib bo'lmadi. Iltimos, qayta urinib ko'ring." + }, + "glanceable": { + "waiting": "Agentlar yangilanmoqda", + "empty": "Bajarilayotgan ish yo'q", + "stale": "Hozir yangilab bo'lmaydi", + "expired": "Holat muddati tugadi", + "signedOut": "Agentlarni ko'rish uchun tizimga kiring", + "privacy": "Agentlar yashirilgan", + "openAgents": "Agentlarni oching", + "running": "Ishlamoqda", + "needsInput": "kiritish kerak", + "reconnecting": "Qayta ulanmoqda", + "channelName": "Faol agentlar", + "activityKitDisabledTitle": "Jonli faoliyatlar o'chirilgan", + "activityKitDisabledBody": "Qulflangan ekranda Faol agentlarni ko'rish uchun Sozlamalarda Jonli faoliyatlarni yoqing." } } diff --git a/apps/mobile/src/i18n/locales/vi.json b/apps/mobile/src/i18n/locales/vi.json index c1dbdfeb3b..3f06694011 100644 --- a/apps/mobile/src/i18n/locales/vi.json +++ b/apps/mobile/src/i18n/locales/vi.json @@ -3191,5 +3191,20 @@ "historyRetry": "Không thể tải lịch sử trò chuyện.", "catalogRetry": "Không thể tải các mô hình", "sendError": "Không thể gửi tin nhắn. Vui lòng thử lại." + }, + "glanceable": { + "waiting": "Đang cập nhật tác nhân", + "empty": "Không có công việc đang thực hiện", + "stale": "Hiện không thể cập nhật", + "expired": "Trạng thái đã hết hạn", + "signedOut": "Đăng nhập để xem tác nhân", + "privacy": "Đã ẩn tác nhân", + "openAgents": "Mở tác nhân", + "running": "ĐANG CHẠY", + "needsInput": "cần nhập", + "reconnecting": "Đang kết nối lại", + "channelName": "Tác nhân đang hoạt động", + "activityKitDisabledTitle": "Hoạt động trực tiếp đang tắt", + "activityKitDisabledBody": "Bật Hoạt động trực tiếp trong Cài đặt để xem các tác nhân đang hoạt động trên Màn hình khóa." } } diff --git a/apps/mobile/src/i18n/locales/yo.json b/apps/mobile/src/i18n/locales/yo.json index 8ec8e7366f..fb68588575 100644 --- a/apps/mobile/src/i18n/locales/yo.json +++ b/apps/mobile/src/i18n/locales/yo.json @@ -3191,5 +3191,20 @@ "historyRetry": "Kò lè ṣagbewọ ìtàn iwiregbe.", "catalogRetry": "Kò lè gbé àwọn àwoṣe", "sendError": "Kò lè ran ifiranṣẹ náà. Jọ̀wọ́ tún gbìyànjú." + }, + "glanceable": { + "waiting": "Ti n ṣe imudojuiwọn awọn aṣoju", + "empty": "Ko si iṣẹ ti n lọ lọwọ", + "stale": "Ko le ṣe imudojuiwọn bayi", + "expired": "Ipo ti pari akoko", + "signedOut": "Wọle lati ri awọn aṣoju", + "privacy": "Awọn aṣoju wa ni ipamọ", + "openAgents": "Ṣii awọn aṣoju", + "running": "ǸJẸ́ ṢÍṢIṢẸ́", + "needsInput": "nilo igbewọle", + "reconnecting": "Ti n tun sopọ", + "channelName": "Awọn aṣoju to n ṣiṣẹ", + "activityKitDisabledTitle": "Awọn Iṣẹ Lọwọlọwọ wa ni pipa", + "activityKitDisabledBody": "Tan Awọn Iṣẹ Lọwọlọwọ ninu Eto lati ri Awọn aṣoju to n ṣiṣẹ lori Iboju Titiipa." } } diff --git a/apps/mobile/src/i18n/locales/zh-Hans.json b/apps/mobile/src/i18n/locales/zh-Hans.json index 0073963026..6f4c98633c 100644 --- a/apps/mobile/src/i18n/locales/zh-Hans.json +++ b/apps/mobile/src/i18n/locales/zh-Hans.json @@ -3191,5 +3191,20 @@ "historyRetry": "无法加载聊天历史。", "catalogRetry": "无法加载模型", "sendError": "无法发送消息。请重试。" + }, + "glanceable": { + "waiting": "正在更新代理", + "empty": "暂无进行中的任务", + "stale": "暂时无法更新", + "expired": "状态已过期", + "signedOut": "请登录以查看代理", + "privacy": "代理已隐藏", + "openAgents": "打开代理", + "running": "运行中", + "needsInput": "需要输入", + "reconnecting": "正在重新连接", + "channelName": "活动代理", + "activityKitDisabledTitle": "实时活动已关闭", + "activityKitDisabledBody": "请在“设置”中开启“实时活动”,以在锁定屏幕上查看活动代理。" } } diff --git a/apps/mobile/src/i18n/locales/zh-Hant.json b/apps/mobile/src/i18n/locales/zh-Hant.json index ad6215bb1f..0ac6b71722 100644 --- a/apps/mobile/src/i18n/locales/zh-Hant.json +++ b/apps/mobile/src/i18n/locales/zh-Hant.json @@ -3191,5 +3191,20 @@ "historyRetry": "無法載入聊天記錄。", "catalogRetry": "無法載入模型", "sendError": "無法傳送訊息。請再試一次。" + }, + "glanceable": { + "waiting": "正在更新代理", + "empty": "沒有進行中的工作", + "stale": "目前無法更新", + "expired": "狀態已過期", + "signedOut": "請登入以查看代理", + "privacy": "代理已隱藏", + "openAgents": "開啟代理", + "running": "執行中", + "needsInput": "需要輸入", + "reconnecting": "正在重新連線", + "channelName": "使用中的代理", + "activityKitDisabledTitle": "即時動態已關閉", + "activityKitDisabledBody": "請在「設定」中開啟「即時動態」,以在鎖定畫面上查看使用中的代理。" } } diff --git a/apps/mobile/src/i18n/locales/zu.json b/apps/mobile/src/i18n/locales/zu.json index 1b1259c2d2..1fbf891bb2 100644 --- a/apps/mobile/src/i18n/locales/zu.json +++ b/apps/mobile/src/i18n/locales/zu.json @@ -3191,5 +3191,20 @@ "historyRetry": "Asikwazanga ukulayisha umlando wengxoxo.", "catalogRetry": "Akukwazanga ukulayisha amamodeli", "sendError": "Asikwazanga ukuthumela umlayezo. Uzama futhi." + }, + "glanceable": { + "waiting": "Ibuyekeza ama-agent", + "empty": "Awukho umsebenzi oqhubekayo", + "stale": "Akukwazi ukubuyekeza manje", + "expired": "Isimo siphelelwe yisikhathi", + "signedOut": "Ngena ngemvume ukuze ubone ama-agent", + "privacy": "Ama-agent afihliwe", + "openAgents": "Vula ama-agent", + "running": "IYASEBENZA", + "needsInput": "idinga okokufaka", + "reconnecting": "Ixhuma kabusha", + "channelName": "Ama-agent asebenzayo", + "activityKitDisabledTitle": "Imisebenzi Ebukhoma ivaliwe", + "activityKitDisabledBody": "Vula Imisebenzi Ebukhoma ku-Izilungiselelo ukuze ubone Ama-agent asebenzayo Esikrinini Esikhiyiwe." } } From af856729305255833e4842cadd6aa7e65ed0ac6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Fri, 28 Aug 2026 11:26:49 +0200 Subject: [PATCH 5/8] fix(mobile): preserve stale snapshot expiry deadlines --- .../mobile/src/lib/glanceable/cleanup.test.ts | 110 ++++++++++++------ apps/mobile/src/lib/glanceable/cleanup.ts | 15 +-- .../src/lib/glanceable/publisher.test.ts | 107 +++++++++++------ apps/mobile/src/lib/glanceable/publisher.ts | 33 ++++-- 4 files changed, 173 insertions(+), 92 deletions(-) diff --git a/apps/mobile/src/lib/glanceable/cleanup.test.ts b/apps/mobile/src/lib/glanceable/cleanup.test.ts index 00693ce963..6f13311da0 100644 --- a/apps/mobile/src/lib/glanceable/cleanup.test.ts +++ b/apps/mobile/src/lib/glanceable/cleanup.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, it } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { type GlanceableAgentsSnapshot } from '@kilocode/app-shared/glanceable-agents-snapshot'; @@ -8,7 +8,11 @@ import { writePrivacySnapshotAndEnd, writeSignedOutSnapshotAndEnd, } from './cleanup'; -import { _resetGlanceablePersistForTests, _setLastGlanceableSnapshotForTests } from './persist'; +import { + _resetGlanceablePersistForTests, + _setLastGlanceableSnapshotForTests, + getLastGlanceableSnapshot, +} from './persist'; import { type GlanceableSink, registerGlanceableSink, @@ -24,6 +28,7 @@ function makeSink() { const calls: SinkCall[] = []; const sink: GlanceableSink = { publish(snapshot) { + _setLastGlanceableSnapshotForTests(snapshot); calls.push({ type: 'publish', snapshot }); }, startOrUpdate(snapshot) { @@ -46,6 +51,7 @@ function lastSnapshot(calls: SinkCall[]): GlanceableAgentsSnapshot { afterEach(() => { _resetGlanceablePersistForTests(); + vi.useRealTimers(); }); describe('cleanup', () => { @@ -115,7 +121,8 @@ describe('cleanup', () => { ).toBe('none'); }); - it('marks stale (keeps counts) when the org list errors', () => { + it('keeps the original deadline through repeated org list failures', () => { + vi.useFakeTimers(); expect( planOrgFenceAction({ organizationId: 'kept-org', @@ -131,11 +138,12 @@ describe('cleanup', () => { updatedAt: '2026-08-27T00:00:00.000Z', expiresAt: '2026-08-27T08:00:00.000Z', scopeKey: 'deadbeef', - organizationBound: false, + accountEpoch: 7, + organizationBound: true, status: 'happy', running: 2, needsInput: 1, - reconnecting: 0, + reconnecting: 1, eligibleStartedAt: '2026-08-26T23:00:00.000Z', }; _setLastGlanceableSnapshotForTests(seeded); @@ -143,40 +151,74 @@ describe('cleanup', () => { const { sink, calls } = makeSink(); registerGlanceableSink(sink); try { - republishLastSnapshotStale(); - const snapshot = lastSnapshot(calls); - expect(snapshot.status).toBe('stale'); - expect(snapshot.running).toBe(2); - expect(snapshot.needsInput).toBe(1); - expect(snapshot.revision).toBe(4); + for (const [index, now] of [ + '2026-08-27T01:00:00.000Z', + '2026-08-27T07:59:59.999Z', + ].entries()) { + vi.setSystemTime(new Date(now)); + republishLastSnapshotStale(); + expect(lastSnapshot(calls)).toEqual({ ...seeded, revision: index + 4, status: 'stale' }); + } + for (const [index, now] of [ + '2026-08-27T08:00:00.000Z', + '2026-08-27T09:00:00.000Z', + ].entries()) { + vi.setSystemTime(new Date(now)); + republishLastSnapshotStale(); + expect(lastSnapshot(calls)).toEqual({ + ...seeded, + revision: index + 6, + status: 'expired', + running: 0, + needsInput: 0, + reconnecting: 0, + eligibleStartedAt: null, + }); + } } finally { unregisterGlanceableSink(sink); } }); - it('does not overwrite a terminal blank with a stale republish', () => { - const terminal: GlanceableAgentsSnapshot = { - schemaVersion: 1, - revision: 5, - updatedAt: '2026-08-27T00:00:00.000Z', - expiresAt: '2026-08-27T08:00:00.000Z', - scopeKey: 'terminal:privacy', - organizationBound: false, - status: 'privacy', - running: 0, - needsInput: 0, - reconnecting: 0, - eligibleStartedAt: null, - }; - _setLastGlanceableSnapshotForTests(terminal); + it.each(['signed_out', 'privacy', 'expired'] as const)( + 'does not replace %s with stale before or after its deadline', + status => { + vi.useFakeTimers(); + const terminal: GlanceableAgentsSnapshot = { + schemaVersion: 1, + revision: 5, + updatedAt: '2026-08-27T00:00:00.000Z', + expiresAt: '2026-08-27T08:00:00.000Z', + scopeKey: `terminal:${status}`, + organizationBound: false, + status, + running: 0, + needsInput: 0, + reconnecting: 0, + eligibleStartedAt: null, + }; + _setLastGlanceableSnapshotForTests(terminal); - const { sink, calls } = makeSink(); - registerGlanceableSink(sink); - try { - republishLastSnapshotStale(); - expect(calls).toEqual([]); - } finally { - unregisterGlanceableSink(sink); + const { sink } = makeSink(); + registerGlanceableSink(sink); + try { + for (const now of ['2026-08-27T01:00:00.000Z', '2026-08-27T09:00:00.000Z']) { + vi.setSystemTime(new Date(now)); + republishLastSnapshotStale(); + expect(getLastGlanceableSnapshot()).toMatchObject({ + status, + scopeKey: terminal.scopeKey, + updatedAt: terminal.updatedAt, + expiresAt: terminal.expiresAt, + running: 0, + needsInput: 0, + reconnecting: 0, + eligibleStartedAt: null, + }); + } + } finally { + unregisterGlanceableSink(sink); + } } - }); + ); }); diff --git a/apps/mobile/src/lib/glanceable/cleanup.ts b/apps/mobile/src/lib/glanceable/cleanup.ts index 57f985a4a2..ded58ef91b 100644 --- a/apps/mobile/src/lib/glanceable/cleanup.ts +++ b/apps/mobile/src/lib/glanceable/cleanup.ts @@ -5,6 +5,7 @@ import { } from '@kilocode/app-shared/glanceable-agents-snapshot'; import { getLastGlanceableSnapshot } from './persist'; +import { withStatus } from './publisher'; import { getGlanceableSinks } from './sink-registry'; // Monotonic epoch bumped on every terminal blank (signed-out or privacy). The @@ -99,8 +100,8 @@ export function writePrivacySnapshotAndEnd(): void { } /** - * Republish the last snapshot with a stale status (keeps counts). Used by the - * org fence when the org list errors: stale, not lost-org. + * Republish the last snapshot as stale until its deadline, then expired. Used + * by the org fence when the org list errors: stale, not lost-org. */ export function republishLastSnapshotStale(): void { const previous = getLastGlanceableSnapshot(); @@ -112,15 +113,7 @@ export function republishLastSnapshotStale(): void { if (previous.status === 'signed_out' || previous.status === 'privacy') { return; } - const now = Date.now(); - const updatedAt = new Date(now).toISOString(); - const snapshot: GlanceableAgentsSnapshot = { - ...previous, - revision: previous.revision + 1, - updatedAt, - expiresAt: new Date(now + GLANCEABLE_SNAPSHOT_EXPIRY_MS).toISOString(), - status: 'stale', - }; + const snapshot = withStatus(previous, 'stale', Date.now()); for (const sink of getGlanceableSinks()) { sink.publish(snapshot); } diff --git a/apps/mobile/src/lib/glanceable/publisher.test.ts b/apps/mobile/src/lib/glanceable/publisher.test.ts index fedbba8203..6615ea8479 100644 --- a/apps/mobile/src/lib/glanceable/publisher.test.ts +++ b/apps/mobile/src/lib/glanceable/publisher.test.ts @@ -143,46 +143,64 @@ describe('GlanceablePublisher', () => { expect(lastSnapshot(calls, 'publish').status).toBe('empty'); }); - it('keeps counts on stale and hides counts on expired', () => { - const { sink, calls } = makeSink(); - const publisher = new GlanceablePublisher({ sinks: [sink], now: () => NOW }); - publisher.handleSessions([{ status: 'busy' }], PUB_CTX); - publisher.handleFetchError(PUB_CTX); - expect(lastSnapshot(calls, 'publish').status).toBe('stale'); - expect(lastSnapshot(calls, 'publish').running).toBe(1); - + it('keeps the original deadline through repeated failures and stays expired after it', () => { let now = NOW; - const { sink: sink2, calls: calls2 } = makeSink(); - const publisher2 = new GlanceablePublisher({ sinks: [sink2], now: () => now }); - publisher2.handleSessions([{ status: 'busy' }], PUB_CTX); - now = NOW + GLANCEABLE_SNAPSHOT_EXPIRY_MS; - publisher2.handleSessions([{ status: 'busy' }], PUB_CTX); - const expired = calls2.filter( - (call): call is { type: 'publish'; snapshot: GlanceableAgentsSnapshot } => - call.type === 'publish' && call.snapshot.status === 'expired' + const { sink, calls } = makeSink(); + const publisher = new GlanceablePublisher({ sinks: [sink], now: () => now }); + publisher.handleSessions( + [{ status: 'busy' }, { status: 'question' }, { status: 'retry' }], + PUB_CTX ); - expect(expired.length).toBe(1); - expect(expired[0]?.snapshot.running).toBe(0); + const successful = lastSnapshot(calls, 'publish'); + const failures = [ + [60_000, 'stale', 1], + [GLANCEABLE_SNAPSHOT_EXPIRY_MS - 1, 'stale', 1], + [GLANCEABLE_SNAPSHOT_EXPIRY_MS, 'expired', 0], + [GLANCEABLE_SNAPSHOT_EXPIRY_MS + 60_000, 'expired', 0], + ] as const; + for (const [index, [elapsed, status, expectedCount]] of failures.entries()) { + now = NOW + elapsed; + publisher.handleFetchError(PUB_CTX); + expect(lastSnapshot(calls, 'publish')).toMatchObject({ + revision: index + 2, + updatedAt: successful.updatedAt, + expiresAt: successful.expiresAt, + scopeKey: successful.scopeKey, + status, + running: expectedCount, + needsInput: expectedCount, + reconnecting: expectedCount, + }); + } + expect(lastSnapshot(calls, 'publish').eligibleStartedAt).toBeNull(); + publisher.dispose(); }); - it('does not schedule the 8s terminal for a signed-out snapshot', () => { - vi.useFakeTimers(); - const { sink, calls } = makeSink(); - const publisher = new GlanceablePublisher({ sinks: [sink], now: () => NOW }); - publisher.applySnapshot( - buildGlanceableSnapshot({ + it.each(['signed_out', 'privacy'] as const)( + 'preserves %s through failures and expiry', + status => { + vi.useFakeTimers(); + let now = NOW; + const { sink, calls } = makeSink(); + const publisher = new GlanceablePublisher({ sinks: [sink], now: () => now }); + const blank = buildGlanceableSnapshot({ sessions: [], userId: 'u1', organizationId: null, now: NOW, - status: 'signed_out', - }), - PUB_CTX - ); - vi.advanceTimersByTime(8000); - expect(count(calls, 'endImmediate')).toBe(0); - publisher.dispose(); - }); + status, + }); + publisher.applySnapshot(blank, PUB_CTX); + for (const elapsed of [60_000, GLANCEABLE_SNAPSHOT_EXPIRY_MS + 1]) { + now = NOW + elapsed; + publisher.handleFetchError(PUB_CTX); + expect(lastSnapshot(calls, 'publish')).toEqual(blank); + } + vi.advanceTimersByTime(8000); + expect(count(calls, 'endImmediate')).toBe(0); + publisher.dispose(); + } + ); it('does not publish or restart after a terminal blank', () => { const { sink, calls } = makeSink(); @@ -201,7 +219,8 @@ describe('GlanceablePublisher', () => { expect(lastSnapshot(calls, 'publish').status).toBe('signed_out'); expect(count(calls, 'endImmediate')).toBe(1); - // A live cache success after the blank must not publish or restart. + // A cache error or success after the blank must not publish or restart. + publisher.handleFetchError(PUB_CTX); publisher.handleSessions([{ status: 'busy' }, { status: 'busy' }], PUB_CTX); expect(count(calls, 'startOrUpdate')).toBe(1); expect(count(calls, 'publish')).toBe(2); @@ -285,13 +304,25 @@ describe('GlanceablePublisher', () => { publisher.dispose(); }); - it('keeps the revision monotonic when seeded from an initial snapshot', () => { + it('renews the deadline on successful data while keeping seeded revisions monotonic', () => { + let now = NOW; const { sink, calls } = makeSink(); - // Seeded with revision 42; the next snapshot must be 43. const initial = snapshotFor([{ status: 'busy' }], NOW - 60_000, 41); - const publisher = new GlanceablePublisher({ sinks: [sink], now: () => NOW, initial }); - publisher.handleSessions([{ status: 'busy' }], PUB_CTX); - expect(lastSnapshot(calls, 'startOrUpdate').revision).toBe(43); + const publisher = new GlanceablePublisher({ sinks: [sink], now: () => now, initial }); + publisher.handleFetchError(PUB_CTX); + publisher.handleSessions([{ status: 'question' }], PUB_CTX); + const fresh = lastSnapshot(calls, 'startOrUpdate'); + expect(fresh).toMatchObject({ + revision: 44, + status: 'happy', + running: 0, + needsInput: 1, + updatedAt: new Date(NOW).toISOString(), + expiresAt: new Date(NOW + GLANCEABLE_SNAPSHOT_EXPIRY_MS).toISOString(), + }); + now = Date.parse(initial.expiresAt); + publisher.handleFetchError(PUB_CTX); + expect(lastSnapshot(calls, 'publish')).toEqual({ ...fresh, revision: 45, status: 'stale' }); publisher.dispose(); }); }); diff --git a/apps/mobile/src/lib/glanceable/publisher.ts b/apps/mobile/src/lib/glanceable/publisher.ts index c4826b4435..e74f241e72 100644 --- a/apps/mobile/src/lib/glanceable/publisher.ts +++ b/apps/mobile/src/lib/glanceable/publisher.ts @@ -40,12 +40,24 @@ export type GlanceablePublisherOptions = { type TimerHandle = ReturnType; -/** Copy a snapshot with a new status and a fresh revision/updatedAt/expiresAt. */ +/** Advance the status and revision without renewing stale data's lifetime. */ export function withStatus( snapshot: GlanceableAgentsSnapshot, status: GlanceableAgentsSnapshotStatus, now: number ): GlanceableAgentsSnapshot { + if (status === 'stale') { + if (snapshot.status === 'signed_out' || snapshot.status === 'privacy') { + return snapshot; + } + const expired = snapshot.status === 'expired' || now >= Date.parse(snapshot.expiresAt); + return { + ...snapshot, + revision: snapshot.revision + 1, + status: expired ? 'expired' : 'stale', + ...(expired ? { running: 0, needsInput: 0, reconnecting: 0, eligibleStartedAt: null } : {}), + }; + } const updatedAt = new Date(now).toISOString(); return { ...snapshot, @@ -140,19 +152,22 @@ export class GlanceablePublisher { this.current = snapshot; } - /** Cache update failed: republish the last counts with a stale status. */ - handleFetchError(ctx: GlanceablePublisherContext): void { - if (this.isGated()) { - return; - } - const now = this.now(); - if (this.applyExpiry(now, ctx) || this.current === null) { + /** Cache update failed: keep the last counts only until their original deadline. */ + handleFetchError(_ctx: GlanceablePublisherContext): void { + if (this.isGated() || this.current === null) { return; } // A fetch error supersedes any pending coalesced happy emit: otherwise the // pre-error snapshot would fire later and overwrite the stale counts. this.cancelCoalesce(); - const snapshot = withStatus(this.current, 'stale', now); + const snapshot = withStatus(this.current, 'stale', this.now()); + if (snapshot === this.current) { + return; + } + if (snapshot.status === 'expired') { + this.cancelTerminal(); + this.activityStarted = false; + } this.publish(snapshot); this.current = snapshot; } From 8f8b7afa8b382d774d9e1c7917a61f52242c5f47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Fri, 28 Aug 2026 13:17:19 +0200 Subject: [PATCH 6/8] fix(i18n): defer native keys to their consuming stack levels --- apps/mobile/src/i18n/locales/af.json | 5 +---- apps/mobile/src/i18n/locales/am.json | 5 +---- apps/mobile/src/i18n/locales/ar.json | 5 +---- apps/mobile/src/i18n/locales/az.json | 5 +---- apps/mobile/src/i18n/locales/be.json | 5 +---- apps/mobile/src/i18n/locales/bg.json | 5 +---- apps/mobile/src/i18n/locales/bn.json | 5 +---- apps/mobile/src/i18n/locales/bs.json | 5 +---- apps/mobile/src/i18n/locales/ca.json | 5 +---- apps/mobile/src/i18n/locales/ckb.json | 5 +---- apps/mobile/src/i18n/locales/cs.json | 5 +---- apps/mobile/src/i18n/locales/cy.json | 5 +---- apps/mobile/src/i18n/locales/da.json | 5 +---- apps/mobile/src/i18n/locales/de.json | 5 +---- apps/mobile/src/i18n/locales/el.json | 5 +---- apps/mobile/src/i18n/locales/en.json | 5 +---- apps/mobile/src/i18n/locales/es.json | 5 +---- apps/mobile/src/i18n/locales/et.json | 5 +---- apps/mobile/src/i18n/locales/eu.json | 5 +---- apps/mobile/src/i18n/locales/fa.json | 5 +---- apps/mobile/src/i18n/locales/fi.json | 5 +---- apps/mobile/src/i18n/locales/fil.json | 5 +---- apps/mobile/src/i18n/locales/fr.json | 5 +---- apps/mobile/src/i18n/locales/ga.json | 5 +---- apps/mobile/src/i18n/locales/gl.json | 5 +---- apps/mobile/src/i18n/locales/gu.json | 5 +---- apps/mobile/src/i18n/locales/ha.json | 5 +---- apps/mobile/src/i18n/locales/he.json | 5 +---- apps/mobile/src/i18n/locales/hi.json | 5 +---- apps/mobile/src/i18n/locales/hr.json | 5 +---- apps/mobile/src/i18n/locales/ht.json | 5 +---- apps/mobile/src/i18n/locales/hu.json | 5 +---- apps/mobile/src/i18n/locales/hy.json | 5 +---- apps/mobile/src/i18n/locales/id.json | 5 +---- apps/mobile/src/i18n/locales/ig.json | 5 +---- apps/mobile/src/i18n/locales/is.json | 5 +---- apps/mobile/src/i18n/locales/it.json | 5 +---- apps/mobile/src/i18n/locales/ja.json | 5 +---- apps/mobile/src/i18n/locales/ka.json | 5 +---- apps/mobile/src/i18n/locales/kk.json | 5 +---- apps/mobile/src/i18n/locales/km.json | 5 +---- apps/mobile/src/i18n/locales/kn.json | 5 +---- apps/mobile/src/i18n/locales/ko.json | 5 +---- apps/mobile/src/i18n/locales/lo.json | 5 +---- apps/mobile/src/i18n/locales/lt.json | 5 +---- apps/mobile/src/i18n/locales/lv.json | 5 +---- apps/mobile/src/i18n/locales/mg.json | 5 +---- apps/mobile/src/i18n/locales/mi.json | 5 +---- apps/mobile/src/i18n/locales/mk.json | 5 +---- apps/mobile/src/i18n/locales/ml.json | 5 +---- apps/mobile/src/i18n/locales/mn.json | 5 +---- apps/mobile/src/i18n/locales/mr.json | 5 +---- apps/mobile/src/i18n/locales/ms.json | 5 +---- apps/mobile/src/i18n/locales/mt.json | 5 +---- apps/mobile/src/i18n/locales/my.json | 5 +---- apps/mobile/src/i18n/locales/nb.json | 5 +---- apps/mobile/src/i18n/locales/ne.json | 5 +---- apps/mobile/src/i18n/locales/nl.json | 5 +---- apps/mobile/src/i18n/locales/om.json | 5 +---- apps/mobile/src/i18n/locales/or.json | 5 +---- apps/mobile/src/i18n/locales/pa.json | 5 +---- apps/mobile/src/i18n/locales/pl.json | 5 +---- apps/mobile/src/i18n/locales/ps.json | 5 +---- apps/mobile/src/i18n/locales/pt-BR.json | 5 +---- apps/mobile/src/i18n/locales/pt.json | 5 +---- apps/mobile/src/i18n/locales/ro.json | 5 +---- apps/mobile/src/i18n/locales/ru.json | 5 +---- apps/mobile/src/i18n/locales/si.json | 5 +---- apps/mobile/src/i18n/locales/sk.json | 5 +---- apps/mobile/src/i18n/locales/sl.json | 5 +---- apps/mobile/src/i18n/locales/so.json | 5 +---- apps/mobile/src/i18n/locales/sq.json | 5 +---- apps/mobile/src/i18n/locales/sr.json | 5 +---- apps/mobile/src/i18n/locales/sv.json | 5 +---- apps/mobile/src/i18n/locales/sw.json | 5 +---- apps/mobile/src/i18n/locales/ta.json | 5 +---- apps/mobile/src/i18n/locales/te.json | 5 +---- apps/mobile/src/i18n/locales/th.json | 5 +---- apps/mobile/src/i18n/locales/tr.json | 5 +---- apps/mobile/src/i18n/locales/uk.json | 5 +---- apps/mobile/src/i18n/locales/ur.json | 5 +---- apps/mobile/src/i18n/locales/uz.json | 5 +---- apps/mobile/src/i18n/locales/vi.json | 5 +---- apps/mobile/src/i18n/locales/yo.json | 5 +---- apps/mobile/src/i18n/locales/zh-Hans.json | 5 +---- apps/mobile/src/i18n/locales/zh-Hant.json | 5 +---- apps/mobile/src/i18n/locales/zu.json | 5 +---- 87 files changed, 87 insertions(+), 348 deletions(-) diff --git a/apps/mobile/src/i18n/locales/af.json b/apps/mobile/src/i18n/locales/af.json index cf670472a7..d6d12d53ba 100644 --- a/apps/mobile/src/i18n/locales/af.json +++ b/apps/mobile/src/i18n/locales/af.json @@ -3202,9 +3202,6 @@ "openAgents": "Maak agente oop", "running": "LOOP", "needsInput": "benodig invoer", - "reconnecting": "Verbind tans weer", - "channelName": "Aktiewe agente", - "activityKitDisabledTitle": "Regstreekse Aktiwiteite is af", - "activityKitDisabledBody": "Skakel Regstreekse Aktiwiteite in Instellings aan om Aktiewe Agente op die Sluitskerm te sien." + "reconnecting": "Verbind tans weer" } } diff --git a/apps/mobile/src/i18n/locales/am.json b/apps/mobile/src/i18n/locales/am.json index 6eb270f961..279c02b9f8 100644 --- a/apps/mobile/src/i18n/locales/am.json +++ b/apps/mobile/src/i18n/locales/am.json @@ -3202,9 +3202,6 @@ "openAgents": "ወኪሎችን ይክፈቱ", "running": "በስራ ላይ", "needsInput": "ግብዓት ይፈልጋል", - "reconnecting": "እንደገና በመገናኘት ላይ", - "channelName": "ንቁ ወኪሎች", - "activityKitDisabledTitle": "የቀጥታ እንቅስቃሴዎች ጠፍተዋል", - "activityKitDisabledBody": "ንቁ ወኪሎችን በመቆለፊያ ማያ ገጽ ላይ ለማየት በቅንብሮች ውስጥ የቀጥታ እንቅስቃሴዎችን ያብሩ።" + "reconnecting": "እንደገና በመገናኘት ላይ" } } diff --git a/apps/mobile/src/i18n/locales/ar.json b/apps/mobile/src/i18n/locales/ar.json index baaef6572f..c78bb4b9fc 100644 --- a/apps/mobile/src/i18n/locales/ar.json +++ b/apps/mobile/src/i18n/locales/ar.json @@ -3286,9 +3286,6 @@ "openAgents": "فتح الوكلاء", "running": "قيد التشغيل", "needsInput": "يتطلب إدخالًا", - "reconnecting": "جارٍ إعادة الاتصال", - "channelName": "الوكلاء النشطون", - "activityKitDisabledTitle": "الأنشطة المباشرة متوقفة", - "activityKitDisabledBody": "فعّل الأنشطة المباشرة في الإعدادات لرؤية الوكلاء النشطين على شاشة القفل." + "reconnecting": "جارٍ إعادة الاتصال" } } diff --git a/apps/mobile/src/i18n/locales/az.json b/apps/mobile/src/i18n/locales/az.json index 760a5f472b..83b88830e0 100644 --- a/apps/mobile/src/i18n/locales/az.json +++ b/apps/mobile/src/i18n/locales/az.json @@ -3202,9 +3202,6 @@ "openAgents": "Agentləri açın", "running": "İŞLƏYİR", "needsInput": "GİRİŞ TƏLƏB OLUNUR", - "reconnecting": "Yenidən qoşulur", - "channelName": "Aktiv agentlər", - "activityKitDisabledTitle": "Canlı fəaliyyətlər söndürülüb", - "activityKitDisabledBody": "Kilid ekranında aktiv agentləri görmək üçün Parametrlərdə Canlı fəaliyyətləri aktivləşdirin." + "reconnecting": "Yenidən qoşulur" } } diff --git a/apps/mobile/src/i18n/locales/be.json b/apps/mobile/src/i18n/locales/be.json index dd9e35eea3..bf323988fc 100644 --- a/apps/mobile/src/i18n/locales/be.json +++ b/apps/mobile/src/i18n/locales/be.json @@ -3244,9 +3244,6 @@ "openAgents": "Адкрыць агентаў", "running": "ПРАЦУЕ", "needsInput": "патрабуецца ўвод", - "reconnecting": "Паўторнае падключэнне", - "channelName": "Актыўныя агенты", - "activityKitDisabledTitle": "Жывыя дзеянні выключаны", - "activityKitDisabledBody": "Уключыце «Жывыя дзеянні» ў «Наладах», каб бачыць актыўных агентаў на экране блакіроўкі." + "reconnecting": "Паўторнае падключэнне" } } diff --git a/apps/mobile/src/i18n/locales/bg.json b/apps/mobile/src/i18n/locales/bg.json index bd69c06cff..99024530e1 100644 --- a/apps/mobile/src/i18n/locales/bg.json +++ b/apps/mobile/src/i18n/locales/bg.json @@ -3202,9 +3202,6 @@ "openAgents": "Отворете агентите", "running": "Изпълнява се", "needsInput": "изисква въвеждане", - "reconnecting": "Повторно свързване", - "channelName": "Активни агенти", - "activityKitDisabledTitle": "Дейностите на живо са изключени", - "activityKitDisabledBody": "Включете Дейности на живо в Настройки, за да виждате активните агенти на заключения екран." + "reconnecting": "Повторно свързване" } } diff --git a/apps/mobile/src/i18n/locales/bn.json b/apps/mobile/src/i18n/locales/bn.json index c4987395e3..1a683c1555 100644 --- a/apps/mobile/src/i18n/locales/bn.json +++ b/apps/mobile/src/i18n/locales/bn.json @@ -3202,9 +3202,6 @@ "openAgents": "এজেন্টগুলি খুলুন", "running": "চলছে", "needsInput": "ইনপুট প্রয়োজন", - "reconnecting": "পুনরায় সংযোগ করা হচ্ছে", - "channelName": "সক্রিয় এজেন্ট", - "activityKitDisabledTitle": "সরাসরি কার্যকলাপ বন্ধ আছে", - "activityKitDisabledBody": "লক স্ক্রিনে সক্রিয় এজেন্টগুলি দেখতে সেটিংসে সরাসরি কার্যকলাপ চালু করুন।" + "reconnecting": "পুনরায় সংযোগ করা হচ্ছে" } } diff --git a/apps/mobile/src/i18n/locales/bs.json b/apps/mobile/src/i18n/locales/bs.json index df8241f41a..ad2c5bb2ee 100644 --- a/apps/mobile/src/i18n/locales/bs.json +++ b/apps/mobile/src/i18n/locales/bs.json @@ -3223,9 +3223,6 @@ "openAgents": "Otvorite agente", "running": "RADI", "needsInput": "treba unos", - "reconnecting": "Ponovno povezivanje", - "channelName": "Aktivni agenti", - "activityKitDisabledTitle": "Aktivnosti uživo su isključene", - "activityKitDisabledBody": "Uključite aktivnosti uživo u Postavkama da biste vidjeli aktivne agente na zaključanom ekranu." + "reconnecting": "Ponovno povezivanje" } } diff --git a/apps/mobile/src/i18n/locales/ca.json b/apps/mobile/src/i18n/locales/ca.json index 8e1ae0ba1b..9e95791e39 100644 --- a/apps/mobile/src/i18n/locales/ca.json +++ b/apps/mobile/src/i18n/locales/ca.json @@ -3223,9 +3223,6 @@ "openAgents": "Obre els agents", "running": "EN EXECUCIÓ", "needsInput": "requereix entrada", - "reconnecting": "Reconnectant", - "channelName": "Agents actius", - "activityKitDisabledTitle": "Les activitats en directe estan desactivades", - "activityKitDisabledBody": "Activa les activitats en directe a Configuració per veure els agents actius a la pantalla de bloqueig." + "reconnecting": "Reconnectant" } } diff --git a/apps/mobile/src/i18n/locales/ckb.json b/apps/mobile/src/i18n/locales/ckb.json index b162480f86..3128184dd0 100644 --- a/apps/mobile/src/i18n/locales/ckb.json +++ b/apps/mobile/src/i18n/locales/ckb.json @@ -3202,9 +3202,6 @@ "openAgents": "کردنەوەی ئەجێنتەکان", "running": "لە کاردایە", "needsInput": "پێویستی بە داخڵکردن", - "reconnecting": "لە پەیوەستبوونەوەدایە", - "channelName": "ئەجێنتە چالاکەکان", - "activityKitDisabledTitle": "چالاکییە ڕاستەوخۆکان ناچالاکن", - "activityKitDisabledBody": "چالاکییە ڕاستەوخۆکان لە ڕێکخستنەکان چالاک بکە بۆ بینینی ئەجێنتە چالاکەکان لە شاشەی قوفڵ." + "reconnecting": "لە پەیوەستبوونەوەدایە" } } diff --git a/apps/mobile/src/i18n/locales/cs.json b/apps/mobile/src/i18n/locales/cs.json index 689e0cef35..5e2eced1b1 100644 --- a/apps/mobile/src/i18n/locales/cs.json +++ b/apps/mobile/src/i18n/locales/cs.json @@ -3244,9 +3244,6 @@ "openAgents": "Otevřít agenty", "running": "BĚŽÍ", "needsInput": "vyžaduje vstup", - "reconnecting": "Obnovování připojení", - "channelName": "Aktivní agenti", - "activityKitDisabledTitle": "Živé aktivity jsou vypnuté", - "activityKitDisabledBody": "Zapněte Živé aktivity v Nastavení, abyste viděli aktivní agenty na zamknuté obrazovce." + "reconnecting": "Obnovování připojení" } } diff --git a/apps/mobile/src/i18n/locales/cy.json b/apps/mobile/src/i18n/locales/cy.json index ae034409c3..5f362a52c6 100644 --- a/apps/mobile/src/i18n/locales/cy.json +++ b/apps/mobile/src/i18n/locales/cy.json @@ -3286,9 +3286,6 @@ "openAgents": "Agorwch asiantau", "running": "YN RHEDEG", "needsInput": "angen mewnbwn", - "reconnecting": "Yn ailgysylltu", - "channelName": "Asiantau gweithredol", - "activityKitDisabledTitle": "Mae Gweithgareddau Byw wedi'u diffodd", - "activityKitDisabledBody": "Trowch Weithgareddau Byw ymlaen yn Gosodiadau i weld Asiantau gweithredol ar y Sgrin Glo." + "reconnecting": "Yn ailgysylltu" } } diff --git a/apps/mobile/src/i18n/locales/da.json b/apps/mobile/src/i18n/locales/da.json index e4e3101fc3..266c995879 100644 --- a/apps/mobile/src/i18n/locales/da.json +++ b/apps/mobile/src/i18n/locales/da.json @@ -3202,9 +3202,6 @@ "openAgents": "Åbn agenter", "running": "KØRER", "needsInput": "kræver input", - "reconnecting": "Genopretter forbindelsen", - "channelName": "Aktive agenter", - "activityKitDisabledTitle": "Liveaktiviteter er slået fra", - "activityKitDisabledBody": "Slå Liveaktiviteter til i Indstillinger for at se aktive agenter på låseskærmen." + "reconnecting": "Genopretter forbindelsen" } } diff --git a/apps/mobile/src/i18n/locales/de.json b/apps/mobile/src/i18n/locales/de.json index 0d65fb86b4..9b5d2d7531 100644 --- a/apps/mobile/src/i18n/locales/de.json +++ b/apps/mobile/src/i18n/locales/de.json @@ -3202,9 +3202,6 @@ "openAgents": "Agenten öffnen", "running": "LÄUFT", "needsInput": "Eingabe erforderlich", - "reconnecting": "Verbindung wird wiederhergestellt", - "channelName": "Aktive Agenten", - "activityKitDisabledTitle": "Live-Aktivitäten sind deaktiviert", - "activityKitDisabledBody": "Aktiviere Live-Aktivitäten in den Einstellungen, um aktive Agenten auf dem Sperrbildschirm zu sehen." + "reconnecting": "Verbindung wird wiederhergestellt" } } diff --git a/apps/mobile/src/i18n/locales/el.json b/apps/mobile/src/i18n/locales/el.json index afc450b50f..1afed8347c 100644 --- a/apps/mobile/src/i18n/locales/el.json +++ b/apps/mobile/src/i18n/locales/el.json @@ -3202,9 +3202,6 @@ "openAgents": "Ανοίξτε τους πράκτορες", "running": "Σε εξέλιξη", "needsInput": "χρειάζεται είσοδο", - "reconnecting": "Επανασύνδεση", - "channelName": "Ενεργοί πράκτορες", - "activityKitDisabledTitle": "Οι Ζωντανές δραστηριότητες είναι απενεργοποιημένες", - "activityKitDisabledBody": "Ενεργοποιήστε τις Ζωντανές δραστηριότητες στις Ρυθμίσεις για να δείτε τους ενεργούς πράκτορες στην Οθόνη κλειδώματος." + "reconnecting": "Επανασύνδεση" } } diff --git a/apps/mobile/src/i18n/locales/en.json b/apps/mobile/src/i18n/locales/en.json index 725ca45c84..511ae214ec 100644 --- a/apps/mobile/src/i18n/locales/en.json +++ b/apps/mobile/src/i18n/locales/en.json @@ -3202,9 +3202,6 @@ "openAgents": "Open agents", "running": "Running", "needsInput": "Needs input", - "reconnecting": "Reconnecting", - "channelName": "Active agents", - "activityKitDisabledTitle": "Live Activities are off", - "activityKitDisabledBody": "Turn on Live Activities in Settings to see Active Agents on the Lock Screen." + "reconnecting": "Reconnecting" } } diff --git a/apps/mobile/src/i18n/locales/es.json b/apps/mobile/src/i18n/locales/es.json index 8eeb9eaa2d..ad24dc8f7a 100644 --- a/apps/mobile/src/i18n/locales/es.json +++ b/apps/mobile/src/i18n/locales/es.json @@ -3223,9 +3223,6 @@ "openAgents": "Abrir agentes", "running": "EN EJECUCIÓN", "needsInput": "requiere entrada", - "reconnecting": "Reconectando", - "channelName": "Agentes activos", - "activityKitDisabledTitle": "Las actividades en directo están desactivadas", - "activityKitDisabledBody": "Activa las actividades en directo en Ajustes para ver los agentes activos en la pantalla de bloqueo." + "reconnecting": "Reconectando" } } diff --git a/apps/mobile/src/i18n/locales/et.json b/apps/mobile/src/i18n/locales/et.json index bcc271ae98..5c6f1acb69 100644 --- a/apps/mobile/src/i18n/locales/et.json +++ b/apps/mobile/src/i18n/locales/et.json @@ -3202,9 +3202,6 @@ "openAgents": "Avage agendid", "running": "TÖÖTAB", "needsInput": "vajab sisendit", - "reconnecting": "Ühenduse taastamine", - "channelName": "Aktiivsed agendid", - "activityKitDisabledTitle": "Reaalajas tegevused on välja lülitatud", - "activityKitDisabledBody": "Lülitage seadetes reaalajas tegevused sisse, et näha aktiivseid agente lukustuskuval." + "reconnecting": "Ühenduse taastamine" } } diff --git a/apps/mobile/src/i18n/locales/eu.json b/apps/mobile/src/i18n/locales/eu.json index 69401ac1c6..6816b5ebdd 100644 --- a/apps/mobile/src/i18n/locales/eu.json +++ b/apps/mobile/src/i18n/locales/eu.json @@ -3202,9 +3202,6 @@ "openAgents": "Ireki agenteak", "running": "Exekutatzen", "needsInput": "sarreraren zain", - "reconnecting": "Berriro konektatzen", - "channelName": "Agente aktiboak", - "activityKitDisabledTitle": "Zuzeneko jarduerak desaktibatuta daude", - "activityKitDisabledBody": "Aktibatu Zuzeneko jarduerak Ezarpenetan, Agente aktiboak Blokeo-pantailan ikusteko." + "reconnecting": "Berriro konektatzen" } } diff --git a/apps/mobile/src/i18n/locales/fa.json b/apps/mobile/src/i18n/locales/fa.json index 4742b0a89b..7f9dfc7d81 100644 --- a/apps/mobile/src/i18n/locales/fa.json +++ b/apps/mobile/src/i18n/locales/fa.json @@ -3202,9 +3202,6 @@ "openAgents": "عامل‌ها را باز کنید", "running": "در حال اجرا", "needsInput": "نیاز به ورودی", - "reconnecting": "در حال اتصال مجدد", - "channelName": "عامل‌های فعال", - "activityKitDisabledTitle": "فعالیت‌های زنده خاموش هستند", - "activityKitDisabledBody": "برای دیدن عامل‌های فعال در صفحه قفل، فعالیت‌های زنده را در تنظیمات روشن کنید." + "reconnecting": "در حال اتصال مجدد" } } diff --git a/apps/mobile/src/i18n/locales/fi.json b/apps/mobile/src/i18n/locales/fi.json index bec672d01b..9574f10953 100644 --- a/apps/mobile/src/i18n/locales/fi.json +++ b/apps/mobile/src/i18n/locales/fi.json @@ -3202,9 +3202,6 @@ "openAgents": "Avaa agentit", "running": "KÄYNNISSÄ", "needsInput": "vaatii syötettä", - "reconnecting": "Yhdistetään uudelleen", - "channelName": "Aktiiviset agentit", - "activityKitDisabledTitle": "Live-aktiviteetit ovat pois päältä", - "activityKitDisabledBody": "Ota live-aktiviteetit käyttöön Asetuksissa, niin näet aktiiviset agentit lukitulla näytöllä." + "reconnecting": "Yhdistetään uudelleen" } } diff --git a/apps/mobile/src/i18n/locales/fil.json b/apps/mobile/src/i18n/locales/fil.json index 0aa3f3643a..0fb827ddc8 100644 --- a/apps/mobile/src/i18n/locales/fil.json +++ b/apps/mobile/src/i18n/locales/fil.json @@ -3202,9 +3202,6 @@ "openAgents": "Buksan ang mga agent", "running": "TUMATAKBO", "needsInput": "kailangan ng input", - "reconnecting": "Muling kumokonekta", - "channelName": "Mga aktibong agent", - "activityKitDisabledTitle": "Naka-off ang Mga Live na Aktibidad", - "activityKitDisabledBody": "I-on ang Mga Live na Aktibidad sa Mga setting para makita ang Mga aktibong agent sa Naka-lock na Screen." + "reconnecting": "Muling kumokonekta" } } diff --git a/apps/mobile/src/i18n/locales/fr.json b/apps/mobile/src/i18n/locales/fr.json index a062104a15..87b5af0562 100644 --- a/apps/mobile/src/i18n/locales/fr.json +++ b/apps/mobile/src/i18n/locales/fr.json @@ -3223,9 +3223,6 @@ "openAgents": "Ouvrir les agents", "running": "EN COURS", "needsInput": "saisie requise", - "reconnecting": "Reconnexion en cours", - "channelName": "Agents actifs", - "activityKitDisabledTitle": "Les activités en direct sont désactivées", - "activityKitDisabledBody": "Activez les activités en direct dans Réglages pour voir les agents actifs sur l'écran verrouillé." + "reconnecting": "Reconnexion en cours" } } diff --git a/apps/mobile/src/i18n/locales/ga.json b/apps/mobile/src/i18n/locales/ga.json index 0e54003877..a240f956b2 100644 --- a/apps/mobile/src/i18n/locales/ga.json +++ b/apps/mobile/src/i18n/locales/ga.json @@ -3265,9 +3265,6 @@ "openAgents": "Oscail gníomhairí", "running": "AG RITH", "needsInput": "teastaíonn ionchur", - "reconnecting": "Ag athcheangal", - "channelName": "Gníomhairí gníomhacha", - "activityKitDisabledTitle": "Tá Gníomhaíochtaí Beo as", - "activityKitDisabledBody": "Cumasaigh Gníomhaíochtaí Beo sna Socruithe chun Gníomhairí gníomhacha a fheiceáil ar an Scáileán Glasála." + "reconnecting": "Ag athcheangal" } } diff --git a/apps/mobile/src/i18n/locales/gl.json b/apps/mobile/src/i18n/locales/gl.json index e59937cb68..26faac0f14 100644 --- a/apps/mobile/src/i18n/locales/gl.json +++ b/apps/mobile/src/i18n/locales/gl.json @@ -3202,9 +3202,6 @@ "openAgents": "Abrir axentes", "running": "Executando", "needsInput": "precisa entrada", - "reconnecting": "Reconectando", - "channelName": "Axentes activos", - "activityKitDisabledTitle": "As actividades en directo están desactivadas", - "activityKitDisabledBody": "Activa as actividades en directo en Configuración para ver os axentes activos na pantalla de bloqueo." + "reconnecting": "Reconectando" } } diff --git a/apps/mobile/src/i18n/locales/gu.json b/apps/mobile/src/i18n/locales/gu.json index b23be114ea..189a6d3be7 100644 --- a/apps/mobile/src/i18n/locales/gu.json +++ b/apps/mobile/src/i18n/locales/gu.json @@ -3202,9 +3202,6 @@ "openAgents": "એજન્ટો ખોલો", "running": "ચાલી રહ્યું છે", "needsInput": "ઇનપુટ જરૂરી", - "reconnecting": "ફરી કનેક્ટ થઈ રહ્યું છે", - "channelName": "સક્રિય એજન્ટો", - "activityKitDisabledTitle": "લાઇવ પ્રવૃત્તિઓ બંધ છે", - "activityKitDisabledBody": "લૉક સ્ક્રીન પર સક્રિય એજન્ટો જોવા માટે સેટિંગ્સમાં લાઇવ પ્રવૃત્તિઓ ચાલુ કરો." + "reconnecting": "ફરી કનેક્ટ થઈ રહ્યું છે" } } diff --git a/apps/mobile/src/i18n/locales/ha.json b/apps/mobile/src/i18n/locales/ha.json index 0707d0bb16..9db5546541 100644 --- a/apps/mobile/src/i18n/locales/ha.json +++ b/apps/mobile/src/i18n/locales/ha.json @@ -3202,9 +3202,6 @@ "openAgents": "Buɗe wakilai", "running": "Ana gudana", "needsInput": "yana buƙatar bayani", - "reconnecting": "Ana sake haɗawa", - "channelName": "Wakilai da ke aiki", - "activityKitDisabledTitle": "Ayyukan Kai Tsaye suna a kashe", - "activityKitDisabledBody": "Kunna Ayyukan Kai Tsaye a cikin Saituna don ganin Wakilai da ke Aiki a kan Allon Kulle." + "reconnecting": "Ana sake haɗawa" } } diff --git a/apps/mobile/src/i18n/locales/he.json b/apps/mobile/src/i18n/locales/he.json index f673155d73..506cad5462 100644 --- a/apps/mobile/src/i18n/locales/he.json +++ b/apps/mobile/src/i18n/locales/he.json @@ -3223,9 +3223,6 @@ "openAgents": "פתח סוכנים", "running": "רץ", "needsInput": "נדרש קלט", - "reconnecting": "מתחבר מחדש", - "channelName": "סוכנים פעילים", - "activityKitDisabledTitle": "פעילויות בזמן אמת כבויות", - "activityKitDisabledBody": "הפעל פעילויות בזמן אמת בהגדרות כדי לראות סוכנים פעילים במסך הנעילה." + "reconnecting": "מתחבר מחדש" } } diff --git a/apps/mobile/src/i18n/locales/hi.json b/apps/mobile/src/i18n/locales/hi.json index 63e95174cc..60947511e1 100644 --- a/apps/mobile/src/i18n/locales/hi.json +++ b/apps/mobile/src/i18n/locales/hi.json @@ -3202,9 +3202,6 @@ "openAgents": "एजेंट खोलें", "running": "चालू", "needsInput": "इनपुट आवश्यक", - "reconnecting": "फिर से कनेक्ट हो रहा है", - "channelName": "सक्रिय एजेंट", - "activityKitDisabledTitle": "लाइव ऐक्टिविटी बंद हैं", - "activityKitDisabledBody": "लॉक स्क्रीन पर सक्रिय एजेंट देखने के लिए सेटिंग में लाइव ऐक्टिविटी चालू करें।" + "reconnecting": "फिर से कनेक्ट हो रहा है" } } diff --git a/apps/mobile/src/i18n/locales/hr.json b/apps/mobile/src/i18n/locales/hr.json index 7cb949d93a..263fdaa48c 100644 --- a/apps/mobile/src/i18n/locales/hr.json +++ b/apps/mobile/src/i18n/locales/hr.json @@ -3223,9 +3223,6 @@ "openAgents": "Otvorite agente", "running": "RADI", "needsInput": "treba unos", - "reconnecting": "Ponovno povezivanje", - "channelName": "Aktivni agenti", - "activityKitDisabledTitle": "Aktivnosti uživo su isključene", - "activityKitDisabledBody": "Uključite Aktivnosti uživo u Postavkama kako biste vidjeli aktivne agente na zaključanom zaslonu." + "reconnecting": "Ponovno povezivanje" } } diff --git a/apps/mobile/src/i18n/locales/ht.json b/apps/mobile/src/i18n/locales/ht.json index 92272deb3c..44aee58be6 100644 --- a/apps/mobile/src/i18n/locales/ht.json +++ b/apps/mobile/src/i18n/locales/ht.json @@ -3202,9 +3202,6 @@ "openAgents": "Louvri ajans yo", "running": "AP KOURI", "needsInput": "bezwen input", - "reconnecting": "Ap rekonekte", - "channelName": "Ajans aktif yo", - "activityKitDisabledTitle": "Aktivite an dirèk yo fèmen", - "activityKitDisabledBody": "Aktive Aktivite an dirèk nan Paramèt pou wè Ajans aktif yo sou Ekran bloke a." + "reconnecting": "Ap rekonekte" } } diff --git a/apps/mobile/src/i18n/locales/hu.json b/apps/mobile/src/i18n/locales/hu.json index 72be12dc20..5813696bc2 100644 --- a/apps/mobile/src/i18n/locales/hu.json +++ b/apps/mobile/src/i18n/locales/hu.json @@ -3202,9 +3202,6 @@ "openAgents": "Ügynökök megnyitása", "running": "Folyamatban", "needsInput": "bemenetet igényel", - "reconnecting": "Újracsatlakozás", - "channelName": "Aktív ügynökök", - "activityKitDisabledTitle": "Az Élő tevékenységek ki vannak kapcsolva", - "activityKitDisabledBody": "Kapcsolja be az Élő tevékenységeket a Beállításokban, hogy az aktív ügynökök megjelenjenek a zárolási képernyőn." + "reconnecting": "Újracsatlakozás" } } diff --git a/apps/mobile/src/i18n/locales/hy.json b/apps/mobile/src/i18n/locales/hy.json index fd86d5065b..f30a70dd75 100644 --- a/apps/mobile/src/i18n/locales/hy.json +++ b/apps/mobile/src/i18n/locales/hy.json @@ -3202,9 +3202,6 @@ "openAgents": "Բացեք գործակալները", "running": "Ընթացքի մեջ է", "needsInput": "մուտքագրման կարիք ունի", - "reconnecting": "Կրկին միացում", - "channelName": "Ակտիվ գործակալներ", - "activityKitDisabledTitle": "Ուղիղ ակտիվություններն անջատված են", - "activityKitDisabledBody": "Միացրեք «Ուղիղ ակտիվություններ»-ը Կարգավորումներում՝ ակտիվ գործակալներին կողպման էկրանին տեսնելու համար։" + "reconnecting": "Կրկին միացում" } } diff --git a/apps/mobile/src/i18n/locales/id.json b/apps/mobile/src/i18n/locales/id.json index 53be7b234d..28e02535b6 100644 --- a/apps/mobile/src/i18n/locales/id.json +++ b/apps/mobile/src/i18n/locales/id.json @@ -3202,9 +3202,6 @@ "openAgents": "Buka agen", "running": "BERJALAN", "needsInput": "memerlukan input", - "reconnecting": "Menghubungkan kembali", - "channelName": "Agen aktif", - "activityKitDisabledTitle": "Aktivitas Langsung nonaktif", - "activityKitDisabledBody": "Aktifkan Aktivitas Langsung di Pengaturan untuk melihat Agen Aktif di Layar Terkunci." + "reconnecting": "Menghubungkan kembali" } } diff --git a/apps/mobile/src/i18n/locales/ig.json b/apps/mobile/src/i18n/locales/ig.json index c4aadb9e50..c17e85e58c 100644 --- a/apps/mobile/src/i18n/locales/ig.json +++ b/apps/mobile/src/i18n/locales/ig.json @@ -3202,9 +3202,6 @@ "openAgents": "Mepee ndị ọrụ", "running": "NA-AGBA", "needsInput": "chọrọ ntinye", - "reconnecting": "Na-ejikọ ọzọ", - "channelName": "Ndị ọrụ na-arụ ọrụ", - "activityKitDisabledTitle": "Agbanyụrụ Ihe Omume Dị Ndụ", - "activityKitDisabledBody": "Gbanye Ihe Omume Dị Ndụ na Ntọala iji hụ Ndị ọrụ na-arụ ọrụ na Ihuenyo Mkpọchi." + "reconnecting": "Na-ejikọ ọzọ" } } diff --git a/apps/mobile/src/i18n/locales/is.json b/apps/mobile/src/i18n/locales/is.json index 0988544e0d..0918a986fe 100644 --- a/apps/mobile/src/i18n/locales/is.json +++ b/apps/mobile/src/i18n/locales/is.json @@ -3202,9 +3202,6 @@ "openAgents": "Opna umboð", "running": "Í gangi", "needsInput": "þarfnast inntaks", - "reconnecting": "Tengist aftur", - "channelName": "Virk umboð", - "activityKitDisabledTitle": "Slökkt er á Beinni virkni", - "activityKitDisabledBody": "Kveiktu á Beinni virkni í Stillingum til að sjá Virk umboð á Lásskjánum." + "reconnecting": "Tengist aftur" } } diff --git a/apps/mobile/src/i18n/locales/it.json b/apps/mobile/src/i18n/locales/it.json index a6dff9624e..8aad0e857b 100644 --- a/apps/mobile/src/i18n/locales/it.json +++ b/apps/mobile/src/i18n/locales/it.json @@ -3223,9 +3223,6 @@ "openAgents": "Apri agenti", "running": "IN ESECUZIONE", "needsInput": "richiede input", - "reconnecting": "Riconnessione in corso", - "channelName": "Agenti attivi", - "activityKitDisabledTitle": "Le attività in tempo reale sono disattivate", - "activityKitDisabledBody": "Attiva le attività in tempo reale in Impostazioni per vedere gli agenti attivi sulla schermata di blocco." + "reconnecting": "Riconnessione in corso" } } diff --git a/apps/mobile/src/i18n/locales/ja.json b/apps/mobile/src/i18n/locales/ja.json index 01c1057b01..83ce883471 100644 --- a/apps/mobile/src/i18n/locales/ja.json +++ b/apps/mobile/src/i18n/locales/ja.json @@ -3202,9 +3202,6 @@ "openAgents": "エージェントを開く", "running": "実行中", "needsInput": "入力が必要", - "reconnecting": "再接続中", - "channelName": "アクティブなエージェント", - "activityKitDisabledTitle": "ライブアクティビティはオフです", - "activityKitDisabledBody": "ロック画面にアクティブなエージェントを表示するには、設定でライブアクティビティをオンにしてください。" + "reconnecting": "再接続中" } } diff --git a/apps/mobile/src/i18n/locales/ka.json b/apps/mobile/src/i18n/locales/ka.json index fb467472e1..d4e298981a 100644 --- a/apps/mobile/src/i18n/locales/ka.json +++ b/apps/mobile/src/i18n/locales/ka.json @@ -3202,9 +3202,6 @@ "openAgents": "აგენტების გახსნა", "running": "მუშაობს", "needsInput": "მოითხოვს შეყვანას", - "reconnecting": "კავშირის აღდგენა", - "channelName": "აქტიური აგენტები", - "activityKitDisabledTitle": "ცოცხალი აქტივობები გამორთულია", - "activityKitDisabledBody": "ჩართეთ ცოცხალი აქტივობები პარამეტრებში, რათა დაბლოკვის ეკრანზე აქტიური აგენტები ნახოთ." + "reconnecting": "კავშირის აღდგენა" } } diff --git a/apps/mobile/src/i18n/locales/kk.json b/apps/mobile/src/i18n/locales/kk.json index f6f359ecf1..63398c3da4 100644 --- a/apps/mobile/src/i18n/locales/kk.json +++ b/apps/mobile/src/i18n/locales/kk.json @@ -3202,9 +3202,6 @@ "openAgents": "Агенттерді ашу", "running": "Орындалуда", "needsInput": "енгізу қажет", - "reconnecting": "Қайта қосылуда", - "channelName": "Белсенді агенттер", - "activityKitDisabledTitle": "Тікелей әрекеттер өшірулі", - "activityKitDisabledBody": "Құлыптау экранында белсенді агенттерді көру үшін Параметрлерде тікелей әрекеттерді қосыңыз." + "reconnecting": "Қайта қосылуда" } } diff --git a/apps/mobile/src/i18n/locales/km.json b/apps/mobile/src/i18n/locales/km.json index 2831e9cf43..951b891c95 100644 --- a/apps/mobile/src/i18n/locales/km.json +++ b/apps/mobile/src/i18n/locales/km.json @@ -3202,9 +3202,6 @@ "openAgents": "បើកភ្នាក់ងារ", "running": "កំពុងដំណើរការ", "needsInput": "ត្រូវការបញ្ចូល", - "reconnecting": "កំពុងភ្ជាប់ឡើងវិញ", - "channelName": "ភ្នាក់ងារសកម្ម", - "activityKitDisabledTitle": "សកម្មភាពបន្តផ្ទាល់ត្រូវបានបិទ", - "activityKitDisabledBody": "បើកសកម្មភាពបន្តផ្ទាល់នៅក្នុងការកំណត់ ដើម្បីមើលភ្នាក់ងារសកម្មនៅលើអេក្រង់ចាក់សោ។" + "reconnecting": "កំពុងភ្ជាប់ឡើងវិញ" } } diff --git a/apps/mobile/src/i18n/locales/kn.json b/apps/mobile/src/i18n/locales/kn.json index c91b75e06e..aa081b63d1 100644 --- a/apps/mobile/src/i18n/locales/kn.json +++ b/apps/mobile/src/i18n/locales/kn.json @@ -3202,9 +3202,6 @@ "openAgents": "ಏಜೆಂಟ್‌ಗಳನ್ನು ತೆರೆಯಿರಿ", "running": "ಚಾಲನೆಯಲ್ಲಿದೆ", "needsInput": "ಇನ್‌ಪುಟ್ ಅಗತ್ಯವಿದೆ", - "reconnecting": "ಮರುಸಂಪರ್ಕಿಸಲಾಗುತ್ತಿದೆ", - "channelName": "ಸಕ್ರಿಯ ಏಜೆಂಟ್‌ಗಳು", - "activityKitDisabledTitle": "ನೇರ ಚಟುವಟಿಕೆಗಳು ಆಫ್ ಆಗಿವೆ", - "activityKitDisabledBody": "ಲಾಕ್ ಪರದೆಯಲ್ಲಿ ಸಕ್ರಿಯ ಏಜೆಂಟ್‌ಗಳನ್ನು ನೋಡಲು ಸೆಟ್ಟಿಂಗ್‌ಗಳಲ್ಲಿ ನೇರ ಚಟುವಟಿಕೆಗಳನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಿ." + "reconnecting": "ಮರುಸಂಪರ್ಕಿಸಲಾಗುತ್ತಿದೆ" } } diff --git a/apps/mobile/src/i18n/locales/ko.json b/apps/mobile/src/i18n/locales/ko.json index 9af46ce32a..de38c321ae 100644 --- a/apps/mobile/src/i18n/locales/ko.json +++ b/apps/mobile/src/i18n/locales/ko.json @@ -3202,9 +3202,6 @@ "openAgents": "에이전트 열기", "running": "실행 중", "needsInput": "입력 필요", - "reconnecting": "다시 연결 중", - "channelName": "활성 에이전트", - "activityKitDisabledTitle": "실시간 현황이 꺼져 있습니다", - "activityKitDisabledBody": "잠금 화면에서 활성 에이전트를 보려면 설정에서 실시간 현황을 켜세요." + "reconnecting": "다시 연결 중" } } diff --git a/apps/mobile/src/i18n/locales/lo.json b/apps/mobile/src/i18n/locales/lo.json index 0f5dc14831..8bc6075cd2 100644 --- a/apps/mobile/src/i18n/locales/lo.json +++ b/apps/mobile/src/i18n/locales/lo.json @@ -3202,9 +3202,6 @@ "openAgents": "ເປີດຕົວແທນ", "running": "ກຳລັງດຳເນີນການ", "needsInput": "ຕ້ອງການຂໍ້ມູນເຂົ້າ", - "reconnecting": "ກຳລັງເຊື່ອມຕໍ່ຄືນ", - "channelName": "ຕົວແທນທີ່ກຳລັງເຮັດວຽກ", - "activityKitDisabledTitle": "ກິດຈະກຳສົດປິດຢູ່", - "activityKitDisabledBody": "ເປີດກິດຈະກຳສົດໃນການຕັ້ງຄ່າ ເພື່ອເບິ່ງຕົວແທນທີ່ກຳລັງເຮັດວຽກໃນໜ້າຈໍລັອກ." + "reconnecting": "ກຳລັງເຊື່ອມຕໍ່ຄືນ" } } diff --git a/apps/mobile/src/i18n/locales/lt.json b/apps/mobile/src/i18n/locales/lt.json index 00b614f2ab..16f66764b0 100644 --- a/apps/mobile/src/i18n/locales/lt.json +++ b/apps/mobile/src/i18n/locales/lt.json @@ -3244,9 +3244,6 @@ "openAgents": "Atidaryti agentus", "running": "Vykdoma", "needsInput": "reikia įvesties", - "reconnecting": "Jungiamasi iš naujo", - "channelName": "Aktyvūs agentai", - "activityKitDisabledTitle": "Tiesioginės veiklos išjungtos", - "activityKitDisabledBody": "Nustatymuose įjunkite tiesiogines veiklas, kad užrakinimo ekrane matytumėte aktyvius agentus." + "reconnecting": "Jungiamasi iš naujo" } } diff --git a/apps/mobile/src/i18n/locales/lv.json b/apps/mobile/src/i18n/locales/lv.json index e9da3f9f1a..51f3ea955f 100644 --- a/apps/mobile/src/i18n/locales/lv.json +++ b/apps/mobile/src/i18n/locales/lv.json @@ -3223,9 +3223,6 @@ "openAgents": "Atvērt aģentus", "running": "DARBOJAS", "needsInput": "nepieciešama ievade", - "reconnecting": "Atkārtoti izveido savienojumu", - "channelName": "Aktīvie aģenti", - "activityKitDisabledTitle": "Tiešraides aktivitātes ir izslēgtas", - "activityKitDisabledBody": "Ieslēdz tiešraides aktivitātes iestatījumos, lai bloķēšanas ekrānā redzētu aktīvos aģentus." + "reconnecting": "Atkārtoti izveido savienojumu" } } diff --git a/apps/mobile/src/i18n/locales/mg.json b/apps/mobile/src/i18n/locales/mg.json index dbb5cdcf65..b88b5054e0 100644 --- a/apps/mobile/src/i18n/locales/mg.json +++ b/apps/mobile/src/i18n/locales/mg.json @@ -3202,9 +3202,6 @@ "openAgents": "Sokafy ny agent", "running": "MANDEHA", "needsInput": "mila fampidirana", - "reconnecting": "Mampifandray indray", - "channelName": "Agent mavitrika", - "activityKitDisabledTitle": "Tsy mandeha ny Hetsika Mivantana", - "activityKitDisabledBody": "Alefaso ao amin'ny Fikirana ny Hetsika Mivantana mba hahitana ny Agent Mavitrika eo amin'ny Efijery Fihidy." + "reconnecting": "Mampifandray indray" } } diff --git a/apps/mobile/src/i18n/locales/mi.json b/apps/mobile/src/i18n/locales/mi.json index 48614e612d..996cc80bd4 100644 --- a/apps/mobile/src/i18n/locales/mi.json +++ b/apps/mobile/src/i18n/locales/mi.json @@ -3202,9 +3202,6 @@ "openAgents": "Whakatuwheratia ngā māngai", "running": "Kei te oma", "needsInput": "e hiahia ana ki te whakaurunga", - "reconnecting": "Kei te hono anō", - "channelName": "Ngā māngai hohe", - "activityKitDisabledTitle": "Kua whakawetohia ngā Mahi Mataora", - "activityKitDisabledBody": "Whakakāngia ngā Mahi Mataora i Ngā tautuhinga kia kite i ngā Māngai Hohe i te Mata Maukati." + "reconnecting": "Kei te hono anō" } } diff --git a/apps/mobile/src/i18n/locales/mk.json b/apps/mobile/src/i18n/locales/mk.json index ba85de30a5..b99d7bde6b 100644 --- a/apps/mobile/src/i18n/locales/mk.json +++ b/apps/mobile/src/i18n/locales/mk.json @@ -3202,9 +3202,6 @@ "openAgents": "Отворете ги агентите", "running": "Во тек", "needsInput": "бара внес", - "reconnecting": "Повторно поврзување", - "channelName": "Активни агенти", - "activityKitDisabledTitle": "Активностите во живо се исклучени", - "activityKitDisabledBody": "Вклучете Активности во живо во Поставки за да ги видите активните агенти на заклучениот екран." + "reconnecting": "Повторно поврзување" } } diff --git a/apps/mobile/src/i18n/locales/ml.json b/apps/mobile/src/i18n/locales/ml.json index 9322d2271d..70513a7aab 100644 --- a/apps/mobile/src/i18n/locales/ml.json +++ b/apps/mobile/src/i18n/locales/ml.json @@ -3202,9 +3202,6 @@ "openAgents": "ഏജന്റുകളെ തുറക്കുക", "running": "പ്രവർത്തിക്കുന്നു", "needsInput": "ഇൻപുട്ട് ആവശ്യമാണ്", - "reconnecting": "വീണ്ടും ബന്ധിപ്പിക്കുന്നു", - "channelName": "സജീവ ഏജന്റുകൾ", - "activityKitDisabledTitle": "തത്സമയ പ്രവർത്തനങ്ങൾ ഓഫാണ്", - "activityKitDisabledBody": "ലോക്ക് സ്ക്രീനിൽ സജീവ ഏജന്റുകളെ കാണാൻ ക്രമീകരണങ്ങളിൽ തത്സമയ പ്രവർത്തനങ്ങൾ പ്രവർത്തനക്ഷമമാക്കുക." + "reconnecting": "വീണ്ടും ബന്ധിപ്പിക്കുന്നു" } } diff --git a/apps/mobile/src/i18n/locales/mn.json b/apps/mobile/src/i18n/locales/mn.json index b81eab872a..96bf6fb332 100644 --- a/apps/mobile/src/i18n/locales/mn.json +++ b/apps/mobile/src/i18n/locales/mn.json @@ -3202,9 +3202,6 @@ "openAgents": "Агентуудыг нээх", "running": "АЖИЛЛАЖ БАЙНА", "needsInput": "оролт шаардлагатай", - "reconnecting": "Дахин холбогдож байна", - "channelName": "Идэвхтэй агентууд", - "activityKitDisabledTitle": "Шууд үйл ажиллагаа унтраалттай байна", - "activityKitDisabledBody": "Түгжээтэй дэлгэц дээр Идэвхтэй агентуудыг харахын тулд Тохиргоо хэсэгт Шууд үйл ажиллагааг асаана уу." + "reconnecting": "Дахин холбогдож байна" } } diff --git a/apps/mobile/src/i18n/locales/mr.json b/apps/mobile/src/i18n/locales/mr.json index b938634c68..b2cfb3efd9 100644 --- a/apps/mobile/src/i18n/locales/mr.json +++ b/apps/mobile/src/i18n/locales/mr.json @@ -3202,9 +3202,6 @@ "openAgents": "एजंट्स उघडा", "running": "चालू आहे", "needsInput": "इनपुट आवश्यक", - "reconnecting": "पुन्हा जोडत आहे", - "channelName": "सक्रिय एजंट्स", - "activityKitDisabledTitle": "थेट क्रियाकलाप बंद आहेत", - "activityKitDisabledBody": "लॉक स्क्रीनवर सक्रिय एजंट्स पाहण्यासाठी सेटिंग्जमध्ये थेट क्रियाकलाप सुरू करा." + "reconnecting": "पुन्हा जोडत आहे" } } diff --git a/apps/mobile/src/i18n/locales/ms.json b/apps/mobile/src/i18n/locales/ms.json index f2383b0678..e43ee7f27c 100644 --- a/apps/mobile/src/i18n/locales/ms.json +++ b/apps/mobile/src/i18n/locales/ms.json @@ -3202,9 +3202,6 @@ "openAgents": "Buka ejen", "running": "Sedang berjalan", "needsInput": "perlu input", - "reconnecting": "Menyambung semula", - "channelName": "Ejen aktif", - "activityKitDisabledTitle": "Aktiviti Langsung dimatikan", - "activityKitDisabledBody": "Hidupkan Aktiviti Langsung dalam Tetapan untuk melihat Ejen Aktif pada Skrin Kunci." + "reconnecting": "Menyambung semula" } } diff --git a/apps/mobile/src/i18n/locales/mt.json b/apps/mobile/src/i18n/locales/mt.json index 9de9bda52b..35c4df9481 100644 --- a/apps/mobile/src/i18n/locales/mt.json +++ b/apps/mobile/src/i18n/locales/mt.json @@ -3265,9 +3265,6 @@ "openAgents": "Iftaħ l-aġenti", "running": "Għaddej", "needsInput": "jeħtieġ input", - "reconnecting": "Qed jerġa' jaqbad", - "channelName": "Aġenti attivi", - "activityKitDisabledTitle": "L-Attivitajiet Diretti huma mitfija", - "activityKitDisabledBody": "Ixgħel l-Attivitajiet Diretti fis-Settings biex tara l-Aġenti Attivi fuq l-Iskrin Imsakkar." + "reconnecting": "Qed jerġa' jaqbad" } } diff --git a/apps/mobile/src/i18n/locales/my.json b/apps/mobile/src/i18n/locales/my.json index 0acbfd71ca..edcc519920 100644 --- a/apps/mobile/src/i18n/locales/my.json +++ b/apps/mobile/src/i18n/locales/my.json @@ -3202,9 +3202,6 @@ "openAgents": "agent များကို ဖွင့်ပါ", "running": "လည်ပတ်နေသည်", "needsInput": "ထည့်သွင်းမှု လိုအပ်သည်", - "reconnecting": "ပြန်ချိတ်ဆက်နေသည်", - "channelName": "လုပ်ဆောင်နေသော agent များ", - "activityKitDisabledTitle": "တိုက်ရိုက်လှုပ်ရှားမှုများ ပိတ်ထားသည်", - "activityKitDisabledBody": "သော့ခတ်မျက်နှာပြင်တွင် လုပ်ဆောင်နေသော agent များကို ကြည့်ရန် ဆက်တင်များတွင် တိုက်ရိုက်လှုပ်ရှားမှုများကို ဖွင့်ပါ။" + "reconnecting": "ပြန်ချိတ်ဆက်နေသည်" } } diff --git a/apps/mobile/src/i18n/locales/nb.json b/apps/mobile/src/i18n/locales/nb.json index a4298c5f36..43761b6c59 100644 --- a/apps/mobile/src/i18n/locales/nb.json +++ b/apps/mobile/src/i18n/locales/nb.json @@ -3202,9 +3202,6 @@ "openAgents": "Åpne agenter", "running": "KJØRER", "needsInput": "trenger innspill", - "reconnecting": "Kobler til på nytt", - "channelName": "Aktive agenter", - "activityKitDisabledTitle": "Oppdateringer i sanntid er av", - "activityKitDisabledBody": "Slå på Oppdateringer i sanntid i Innstillinger for å se Aktive agenter på låst skjerm." + "reconnecting": "Kobler til på nytt" } } diff --git a/apps/mobile/src/i18n/locales/ne.json b/apps/mobile/src/i18n/locales/ne.json index ecf5f482fb..de1427f1cc 100644 --- a/apps/mobile/src/i18n/locales/ne.json +++ b/apps/mobile/src/i18n/locales/ne.json @@ -3202,9 +3202,6 @@ "openAgents": "एजेन्टहरू खोल्नुहोस्", "running": "चलिरहेको", "needsInput": "इनपुट चाहिन्छ", - "reconnecting": "पुनः जडान गर्दै", - "channelName": "सक्रिय एजेन्टहरू", - "activityKitDisabledTitle": "प्रत्यक्ष गतिविधिहरू बन्द छन्", - "activityKitDisabledBody": "लक स्क्रिनमा सक्रिय एजेन्टहरू हेर्न सेटिङ्समा प्रत्यक्ष गतिविधिहरू चालू गर्नुहोस्।" + "reconnecting": "पुनः जडान गर्दै" } } diff --git a/apps/mobile/src/i18n/locales/nl.json b/apps/mobile/src/i18n/locales/nl.json index 11b04a90b7..41325b46f0 100644 --- a/apps/mobile/src/i18n/locales/nl.json +++ b/apps/mobile/src/i18n/locales/nl.json @@ -3202,9 +3202,6 @@ "openAgents": "Agents openen", "running": "Bezig", "needsInput": "heeft invoer nodig", - "reconnecting": "Opnieuw verbinden", - "channelName": "Actieve agents", - "activityKitDisabledTitle": "Liveactiviteiten staan uit", - "activityKitDisabledBody": "Schakel liveactiviteiten in via Instellingen om actieve agents op het toegangsscherm te zien." + "reconnecting": "Opnieuw verbinden" } } diff --git a/apps/mobile/src/i18n/locales/om.json b/apps/mobile/src/i18n/locales/om.json index 7e7c873d39..1275746730 100644 --- a/apps/mobile/src/i18n/locales/om.json +++ b/apps/mobile/src/i18n/locales/om.json @@ -3202,9 +3202,6 @@ "openAgents": "Eejentoota banaa", "running": "Hojii irra jira", "needsInput": "seensa barbaada", - "reconnecting": "Irra deebi'ee walqabachaa jira", - "channelName": "Eejentoota hojii irra jiran", - "activityKitDisabledTitle": "Sochiiwwan Kallattii cufamaniiru", - "activityKitDisabledBody": "Eejentoota hojii irra jiran Iskiriinii Qulfii irratti arguuf, Qindaa'ina keessatti Sochiiwwan Kallattii banaa." + "reconnecting": "Irra deebi'ee walqabachaa jira" } } diff --git a/apps/mobile/src/i18n/locales/or.json b/apps/mobile/src/i18n/locales/or.json index 28dfd8f459..5f0ddff01a 100644 --- a/apps/mobile/src/i18n/locales/or.json +++ b/apps/mobile/src/i18n/locales/or.json @@ -3202,9 +3202,6 @@ "openAgents": "ଏଜେଣ୍ଟଗୁଡ଼ିକ ଖୋଲନ୍ତୁ", "running": "ଚାଲୁଛି", "needsInput": "ଇନପୁଟ୍ ଆବଶ୍ୟକ", - "reconnecting": "ପୁଣି ସଂଯୋଗ ହେଉଛି", - "channelName": "ସକ୍ରିୟ ଏଜେଣ୍ଟଗୁଡ଼ିକ", - "activityKitDisabledTitle": "ଲାଇଭ୍ କାର୍ଯ୍ୟକଳାପ ବନ୍ଦ ଅଛି", - "activityKitDisabledBody": "ଲକ୍ ସ୍କ୍ରିନ୍‌ରେ ସକ୍ରିୟ ଏଜେଣ୍ଟଗୁଡ଼ିକ ଦେଖିବାକୁ ସେଟିଂସ୍‌ରେ ଲାଇଭ୍ କାର୍ଯ୍ୟକଳାପ ଚାଲୁ କରନ୍ତୁ।" + "reconnecting": "ପୁଣି ସଂଯୋଗ ହେଉଛି" } } diff --git a/apps/mobile/src/i18n/locales/pa.json b/apps/mobile/src/i18n/locales/pa.json index dc384c98b0..693033ab74 100644 --- a/apps/mobile/src/i18n/locales/pa.json +++ b/apps/mobile/src/i18n/locales/pa.json @@ -3202,9 +3202,6 @@ "openAgents": "ਏਜੰਟ ਖੋਲ੍ਹੋ", "running": "ਚੱਲ ਰਿਹਾ ਹੈ", "needsInput": "ਇਨਪੁੱਟ ਦੀ ਲੋੜ ਹੈ", - "reconnecting": "ਮੁੜ ਕਨੈਕਟ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ", - "channelName": "ਸਰਗਰਮ ਏਜੰਟ", - "activityKitDisabledTitle": "ਲਾਈਵ ਗਤੀਵਿਧੀਆਂ ਬੰਦ ਹਨ", - "activityKitDisabledBody": "ਲਾਕ ਸਕ੍ਰੀਨ 'ਤੇ ਸਰਗਰਮ ਏਜੰਟ ਦੇਖਣ ਲਈ ਸੈਟਿੰਗਾਂ ਵਿੱਚ ਲਾਈਵ ਗਤੀਵਿਧੀਆਂ ਚਾਲੂ ਕਰੋ।" + "reconnecting": "ਮੁੜ ਕਨੈਕਟ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ" } } diff --git a/apps/mobile/src/i18n/locales/pl.json b/apps/mobile/src/i18n/locales/pl.json index e0b439e9c0..cad09ac2c4 100644 --- a/apps/mobile/src/i18n/locales/pl.json +++ b/apps/mobile/src/i18n/locales/pl.json @@ -3244,9 +3244,6 @@ "openAgents": "Otwórz agentów", "running": "W toku", "needsInput": "wymaga danych", - "reconnecting": "Ponowne łączenie", - "channelName": "Aktywni agenci", - "activityKitDisabledTitle": "Wydarzenia na żywo są wyłączone", - "activityKitDisabledBody": "Włącz wydarzenia na żywo w Ustawieniach, aby widzieć aktywnych agentów na ekranie blokady." + "reconnecting": "Ponowne łączenie" } } diff --git a/apps/mobile/src/i18n/locales/ps.json b/apps/mobile/src/i18n/locales/ps.json index 023a93365b..93ecebe60c 100644 --- a/apps/mobile/src/i18n/locales/ps.json +++ b/apps/mobile/src/i18n/locales/ps.json @@ -3202,9 +3202,6 @@ "openAgents": "اجنټان پرانیزئ", "running": "روان", "needsInput": "ورودی ته اړتیا لري", - "reconnecting": "بیا نښلېږي", - "channelName": "فعال اجنټان", - "activityKitDisabledTitle": "ژوندي فعالیتونه بند دي", - "activityKitDisabledBody": "په قلف شوې پرده کې د فعالو اجنټانو د لیدلو لپاره په ترتیباتو کې ژوندي فعالیتونه فعال کړئ." + "reconnecting": "بیا نښلېږي" } } diff --git a/apps/mobile/src/i18n/locales/pt-BR.json b/apps/mobile/src/i18n/locales/pt-BR.json index ce93386fce..a90088c38b 100644 --- a/apps/mobile/src/i18n/locales/pt-BR.json +++ b/apps/mobile/src/i18n/locales/pt-BR.json @@ -3223,9 +3223,6 @@ "openAgents": "Abrir agentes", "running": "Em execução", "needsInput": "requer entrada", - "reconnecting": "Reconectando", - "channelName": "Agentes ativos", - "activityKitDisabledTitle": "As Atividades ao Vivo estão desativadas", - "activityKitDisabledBody": "Ative as Atividades ao Vivo em Ajustes para ver os agentes ativos na Tela Bloqueada." + "reconnecting": "Reconectando" } } diff --git a/apps/mobile/src/i18n/locales/pt.json b/apps/mobile/src/i18n/locales/pt.json index 73f9fcdd8c..b046797fb2 100644 --- a/apps/mobile/src/i18n/locales/pt.json +++ b/apps/mobile/src/i18n/locales/pt.json @@ -3223,9 +3223,6 @@ "openAgents": "Abrir agentes", "running": "EM EXECUÇÃO", "needsInput": "requer entrada", - "reconnecting": "A restabelecer ligação", - "channelName": "Agentes ativos", - "activityKitDisabledTitle": "As Atividades em tempo real estão desativadas", - "activityKitDisabledBody": "Ative as Atividades em tempo real nas Definições para ver os agentes ativos no Ecrã bloqueado." + "reconnecting": "A restabelecer ligação" } } diff --git a/apps/mobile/src/i18n/locales/ro.json b/apps/mobile/src/i18n/locales/ro.json index f1a3aa9777..edc32efe7a 100644 --- a/apps/mobile/src/i18n/locales/ro.json +++ b/apps/mobile/src/i18n/locales/ro.json @@ -3223,9 +3223,6 @@ "openAgents": "Deschide agenții", "running": "Rulează", "needsInput": "necesită introducere", - "reconnecting": "Se reconectează", - "channelName": "Agenți activi", - "activityKitDisabledTitle": "Activitățile live sunt dezactivate", - "activityKitDisabledBody": "Activează Activități live în Setări pentru a vedea Agenții activi pe ecranul de blocare." + "reconnecting": "Se reconectează" } } diff --git a/apps/mobile/src/i18n/locales/ru.json b/apps/mobile/src/i18n/locales/ru.json index 8412bf2ac9..9d0433771e 100644 --- a/apps/mobile/src/i18n/locales/ru.json +++ b/apps/mobile/src/i18n/locales/ru.json @@ -3244,9 +3244,6 @@ "openAgents": "Открыть агентов", "running": "Выполняется", "needsInput": "требует ввода", - "reconnecting": "Повторное подключение", - "channelName": "Активные агенты", - "activityKitDisabledTitle": "Эфир активности выключен", - "activityKitDisabledBody": "Включите Эфир активности в Настройках, чтобы видеть активных агентов на экране блокировки." + "reconnecting": "Повторное подключение" } } diff --git a/apps/mobile/src/i18n/locales/si.json b/apps/mobile/src/i18n/locales/si.json index f8bd34b3e0..b91444bd82 100644 --- a/apps/mobile/src/i18n/locales/si.json +++ b/apps/mobile/src/i18n/locales/si.json @@ -3202,9 +3202,6 @@ "openAgents": "නියෝජිතයන් විවෘත කරන්න", "running": "ධාවනය වෙමින්", "needsInput": "ආදානය අවශ්යයි", - "reconnecting": "නැවත සම්බන්ධ වෙමින්", - "channelName": "සක්‍රිය නියෝජිතයන්", - "activityKitDisabledTitle": "සජීවී ක්‍රියාකාරකම් අක්‍රියයි", - "activityKitDisabledBody": "අගුළු තිරයේ සක්‍රිය නියෝජිතයන් බැලීමට සැකසුම් තුළ සජීවී ක්‍රියාකාරකම් සක්‍රිය කරන්න." + "reconnecting": "නැවත සම්බන්ධ වෙමින්" } } diff --git a/apps/mobile/src/i18n/locales/sk.json b/apps/mobile/src/i18n/locales/sk.json index 5e66e53b27..5a2da6540d 100644 --- a/apps/mobile/src/i18n/locales/sk.json +++ b/apps/mobile/src/i18n/locales/sk.json @@ -3244,9 +3244,6 @@ "openAgents": "Otvoriť agentov", "running": "Prebieha", "needsInput": "vyžaduje vstup", - "reconnecting": "Opätovné pripájanie", - "channelName": "Aktívni agenti", - "activityKitDisabledTitle": "Živé aktivity sú vypnuté", - "activityKitDisabledBody": "Zapnite živé aktivity v Nastaveniach, aby sa aktívni agenti zobrazovali na zamknutej obrazovke." + "reconnecting": "Opätovné pripájanie" } } diff --git a/apps/mobile/src/i18n/locales/sl.json b/apps/mobile/src/i18n/locales/sl.json index 4cc7c05f56..6138aa0a4d 100644 --- a/apps/mobile/src/i18n/locales/sl.json +++ b/apps/mobile/src/i18n/locales/sl.json @@ -3244,9 +3244,6 @@ "openAgents": "Odprite agente", "running": "DELUJE", "needsInput": "potrebuje vnos", - "reconnecting": "Ponovno povezovanje", - "channelName": "Aktivni agenti", - "activityKitDisabledTitle": "Dejavnosti v živo so izklopljene", - "activityKitDisabledBody": "V Nastavitvah vklopite Dejavnosti v živo, da bodo Aktivni agenti prikazani na zaklenjenem zaslonu." + "reconnecting": "Ponovno povezovanje" } } diff --git a/apps/mobile/src/i18n/locales/so.json b/apps/mobile/src/i18n/locales/so.json index 218014c494..7b09b06741 100644 --- a/apps/mobile/src/i18n/locales/so.json +++ b/apps/mobile/src/i18n/locales/so.json @@ -3202,9 +3202,6 @@ "openAgents": "Fur wakiillada", "running": "Socodaya", "needsInput": "u baahan wax-soo-gal", - "reconnecting": "Dib u xiriirinaya", - "channelName": "Wakiillada firfircoon", - "activityKitDisabledTitle": "Hawlaha Tooska ah waa daman", - "activityKitDisabledBody": "Ku daar Hawlaha Tooska ah Dejinta si aad Wakiillada firfircoon ugu aragto Shaashadda Qufulka." + "reconnecting": "Dib u xiriirinaya" } } diff --git a/apps/mobile/src/i18n/locales/sq.json b/apps/mobile/src/i18n/locales/sq.json index dacf2759e9..e347f02620 100644 --- a/apps/mobile/src/i18n/locales/sq.json +++ b/apps/mobile/src/i18n/locales/sq.json @@ -3202,9 +3202,6 @@ "openAgents": "Hapni agjentët", "running": "Në ekzekutim", "needsInput": "ka nevojë për të dhëna", - "reconnecting": "Duke u rilidhur", - "channelName": "Agjentët aktivë", - "activityKitDisabledTitle": "Aktivitetet e drejtpërdrejta janë çaktivizuar", - "activityKitDisabledBody": "Aktivizoni Aktivitetet e drejtpërdrejta te Cilësimet për të parë Agjentët aktivë në Ekranin e kyçjes." + "reconnecting": "Duke u rilidhur" } } diff --git a/apps/mobile/src/i18n/locales/sr.json b/apps/mobile/src/i18n/locales/sr.json index 472e29f72b..9ff967432a 100644 --- a/apps/mobile/src/i18n/locales/sr.json +++ b/apps/mobile/src/i18n/locales/sr.json @@ -3223,9 +3223,6 @@ "openAgents": "Otvorite agente", "running": "U toku", "needsInput": "zahteva unos", - "reconnecting": "Ponovno povezivanje", - "channelName": "Aktivni agenti", - "activityKitDisabledTitle": "Aktivnosti uživo su isključene", - "activityKitDisabledBody": "Uključite Aktivnosti uživo u Podešavanjima da biste videli aktivne agente na zaključanom ekranu." + "reconnecting": "Ponovno povezivanje" } } diff --git a/apps/mobile/src/i18n/locales/sv.json b/apps/mobile/src/i18n/locales/sv.json index 5ddf047e59..23133bef21 100644 --- a/apps/mobile/src/i18n/locales/sv.json +++ b/apps/mobile/src/i18n/locales/sv.json @@ -3202,9 +3202,6 @@ "openAgents": "Öppna agenter", "running": "KÖRS", "needsInput": "kräver indata", - "reconnecting": "Återansluter", - "channelName": "Aktiva agenter", - "activityKitDisabledTitle": "Liveaktiviteter är avstängda", - "activityKitDisabledBody": "Aktivera liveaktiviteter i Inställningar för att se Aktiva agenter på låsskärmen." + "reconnecting": "Återansluter" } } diff --git a/apps/mobile/src/i18n/locales/sw.json b/apps/mobile/src/i18n/locales/sw.json index 4b57985a59..074cfb9315 100644 --- a/apps/mobile/src/i18n/locales/sw.json +++ b/apps/mobile/src/i18n/locales/sw.json @@ -3202,9 +3202,6 @@ "openAgents": "Fungua mawakala", "running": "Inaendelea", "needsInput": "inahitaji mchango", - "reconnecting": "Inaunganisha tena", - "channelName": "Mawakala wanaofanya kazi", - "activityKitDisabledTitle": "Shughuli za Moja kwa Moja zimezimwa", - "activityKitDisabledBody": "Washa Shughuli za Moja kwa Moja katika Mipangilio ili uone Mawakala wanaofanya kazi kwenye Skrini Iliyofungwa." + "reconnecting": "Inaunganisha tena" } } diff --git a/apps/mobile/src/i18n/locales/ta.json b/apps/mobile/src/i18n/locales/ta.json index 71c0233906..1888c8722e 100644 --- a/apps/mobile/src/i18n/locales/ta.json +++ b/apps/mobile/src/i18n/locales/ta.json @@ -3202,9 +3202,6 @@ "openAgents": "முகவர்களைத் திறக்கவும்", "running": "இயங்குகிறது", "needsInput": "உள்ளீடு தேவை", - "reconnecting": "மீண்டும் இணைக்கிறது", - "channelName": "செயலில் உள்ள முகவர்கள்", - "activityKitDisabledTitle": "நேரலைச் செயல்பாடுகள் முடக்கப்பட்டுள்ளன", - "activityKitDisabledBody": "பூட்டுத் திரையில் செயலில் உள்ள முகவர்களைக் காண அமைப்புகளில் நேரலைச் செயல்பாடுகளை இயக்கவும்." + "reconnecting": "மீண்டும் இணைக்கிறது" } } diff --git a/apps/mobile/src/i18n/locales/te.json b/apps/mobile/src/i18n/locales/te.json index 7c83980d8f..27fdafb1b4 100644 --- a/apps/mobile/src/i18n/locales/te.json +++ b/apps/mobile/src/i18n/locales/te.json @@ -3202,9 +3202,6 @@ "openAgents": "ఏజెంట్లను తెరవండి", "running": "నడుస్తోంది", "needsInput": "ఇన్పుట్ అవసరం", - "reconnecting": "మళ్లీ కనెక్ట్ అవుతోంది", - "channelName": "చురుకైన ఏజెంట్లు", - "activityKitDisabledTitle": "ప్రత్యక్ష కార్యకలాపాలు ఆఫ్‌లో ఉన్నాయి", - "activityKitDisabledBody": "లాక్ స్క్రీన్‌పై చురుకైన ఏజెంట్లను చూడటానికి సెట్టింగ్‌లలో ప్రత్యక్ష కార్యకలాపాలను ఆన్ చేయండి." + "reconnecting": "మళ్లీ కనెక్ట్ అవుతోంది" } } diff --git a/apps/mobile/src/i18n/locales/th.json b/apps/mobile/src/i18n/locales/th.json index b1cad084ff..be2437ae64 100644 --- a/apps/mobile/src/i18n/locales/th.json +++ b/apps/mobile/src/i18n/locales/th.json @@ -3202,9 +3202,6 @@ "openAgents": "เปิดเอเจนต์", "running": "กำลังทำงาน", "needsInput": "ต้องป้อนข้อมูล", - "reconnecting": "กำลังเชื่อมต่อใหม่", - "channelName": "เอเจนต์ที่กำลังทำงาน", - "activityKitDisabledTitle": "กิจกรรมสดปิดอยู่", - "activityKitDisabledBody": "เปิดกิจกรรมสดในการตั้งค่าเพื่อดูเอเจนต์ที่กำลังทำงานบนหน้าจอล็อค" + "reconnecting": "กำลังเชื่อมต่อใหม่" } } diff --git a/apps/mobile/src/i18n/locales/tr.json b/apps/mobile/src/i18n/locales/tr.json index ea85addb9b..7117b3d714 100644 --- a/apps/mobile/src/i18n/locales/tr.json +++ b/apps/mobile/src/i18n/locales/tr.json @@ -3202,9 +3202,6 @@ "openAgents": "Ajanları açın", "running": "Çalışıyor", "needsInput": "Girdi gerekli", - "reconnecting": "Yeniden bağlanılıyor", - "channelName": "Etkin ajanlar", - "activityKitDisabledTitle": "Canlı Etkinlikler kapalı", - "activityKitDisabledBody": "Etkin ajanları Kilit Ekranı'nda görmek için Ayarlar'dan Canlı Etkinlikler'i açın." + "reconnecting": "Yeniden bağlanılıyor" } } diff --git a/apps/mobile/src/i18n/locales/uk.json b/apps/mobile/src/i18n/locales/uk.json index 3a959178d8..01b6312b42 100644 --- a/apps/mobile/src/i18n/locales/uk.json +++ b/apps/mobile/src/i18n/locales/uk.json @@ -3244,9 +3244,6 @@ "openAgents": "Відкрити агентів", "running": "Виконується", "needsInput": "потребує вводу", - "reconnecting": "Повторне підключення", - "channelName": "Активні агенти", - "activityKitDisabledTitle": "Дії наживо вимкнено", - "activityKitDisabledBody": "Увімкніть «Дії наживо» в «Параметрах», щоб бачити активних агентів на замкненому екрані." + "reconnecting": "Повторне підключення" } } diff --git a/apps/mobile/src/i18n/locales/ur.json b/apps/mobile/src/i18n/locales/ur.json index 7acf73692f..4603f57ea1 100644 --- a/apps/mobile/src/i18n/locales/ur.json +++ b/apps/mobile/src/i18n/locales/ur.json @@ -3202,9 +3202,6 @@ "openAgents": "ایجنٹس کھولیں", "running": "چل رہا ہے", "needsInput": "ان پٹ درکار", - "reconnecting": "دوبارہ منسلک ہو رہا ہے", - "channelName": "فعال ایجنٹس", - "activityKitDisabledTitle": "لائیو سرگرمیاں بند ہیں", - "activityKitDisabledBody": "لاک اسکرین پر فعال ایجنٹس دیکھنے کے لیے ترتیبات میں لائیو سرگرمیاں فعال کریں۔" + "reconnecting": "دوبارہ منسلک ہو رہا ہے" } } diff --git a/apps/mobile/src/i18n/locales/uz.json b/apps/mobile/src/i18n/locales/uz.json index 550efd65b8..eaa3609da4 100644 --- a/apps/mobile/src/i18n/locales/uz.json +++ b/apps/mobile/src/i18n/locales/uz.json @@ -3202,9 +3202,6 @@ "openAgents": "Agentlarni oching", "running": "Ishlamoqda", "needsInput": "kiritish kerak", - "reconnecting": "Qayta ulanmoqda", - "channelName": "Faol agentlar", - "activityKitDisabledTitle": "Jonli faoliyatlar o'chirilgan", - "activityKitDisabledBody": "Qulflangan ekranda Faol agentlarni ko'rish uchun Sozlamalarda Jonli faoliyatlarni yoqing." + "reconnecting": "Qayta ulanmoqda" } } diff --git a/apps/mobile/src/i18n/locales/vi.json b/apps/mobile/src/i18n/locales/vi.json index 3f06694011..86c8419a80 100644 --- a/apps/mobile/src/i18n/locales/vi.json +++ b/apps/mobile/src/i18n/locales/vi.json @@ -3202,9 +3202,6 @@ "openAgents": "Mở tác nhân", "running": "ĐANG CHẠY", "needsInput": "cần nhập", - "reconnecting": "Đang kết nối lại", - "channelName": "Tác nhân đang hoạt động", - "activityKitDisabledTitle": "Hoạt động trực tiếp đang tắt", - "activityKitDisabledBody": "Bật Hoạt động trực tiếp trong Cài đặt để xem các tác nhân đang hoạt động trên Màn hình khóa." + "reconnecting": "Đang kết nối lại" } } diff --git a/apps/mobile/src/i18n/locales/yo.json b/apps/mobile/src/i18n/locales/yo.json index fb68588575..eb769c9306 100644 --- a/apps/mobile/src/i18n/locales/yo.json +++ b/apps/mobile/src/i18n/locales/yo.json @@ -3202,9 +3202,6 @@ "openAgents": "Ṣii awọn aṣoju", "running": "ǸJẸ́ ṢÍṢIṢẸ́", "needsInput": "nilo igbewọle", - "reconnecting": "Ti n tun sopọ", - "channelName": "Awọn aṣoju to n ṣiṣẹ", - "activityKitDisabledTitle": "Awọn Iṣẹ Lọwọlọwọ wa ni pipa", - "activityKitDisabledBody": "Tan Awọn Iṣẹ Lọwọlọwọ ninu Eto lati ri Awọn aṣoju to n ṣiṣẹ lori Iboju Titiipa." + "reconnecting": "Ti n tun sopọ" } } diff --git a/apps/mobile/src/i18n/locales/zh-Hans.json b/apps/mobile/src/i18n/locales/zh-Hans.json index 6f4c98633c..3fb3b32458 100644 --- a/apps/mobile/src/i18n/locales/zh-Hans.json +++ b/apps/mobile/src/i18n/locales/zh-Hans.json @@ -3202,9 +3202,6 @@ "openAgents": "打开代理", "running": "运行中", "needsInput": "需要输入", - "reconnecting": "正在重新连接", - "channelName": "活动代理", - "activityKitDisabledTitle": "实时活动已关闭", - "activityKitDisabledBody": "请在“设置”中开启“实时活动”,以在锁定屏幕上查看活动代理。" + "reconnecting": "正在重新连接" } } diff --git a/apps/mobile/src/i18n/locales/zh-Hant.json b/apps/mobile/src/i18n/locales/zh-Hant.json index 0ac6b71722..5d43030b65 100644 --- a/apps/mobile/src/i18n/locales/zh-Hant.json +++ b/apps/mobile/src/i18n/locales/zh-Hant.json @@ -3202,9 +3202,6 @@ "openAgents": "開啟代理", "running": "執行中", "needsInput": "需要輸入", - "reconnecting": "正在重新連線", - "channelName": "使用中的代理", - "activityKitDisabledTitle": "即時動態已關閉", - "activityKitDisabledBody": "請在「設定」中開啟「即時動態」,以在鎖定畫面上查看使用中的代理。" + "reconnecting": "正在重新連線" } } diff --git a/apps/mobile/src/i18n/locales/zu.json b/apps/mobile/src/i18n/locales/zu.json index 1fbf891bb2..89fb9ef2c9 100644 --- a/apps/mobile/src/i18n/locales/zu.json +++ b/apps/mobile/src/i18n/locales/zu.json @@ -3202,9 +3202,6 @@ "openAgents": "Vula ama-agent", "running": "IYASEBENZA", "needsInput": "idinga okokufaka", - "reconnecting": "Ixhuma kabusha", - "channelName": "Ama-agent asebenzayo", - "activityKitDisabledTitle": "Imisebenzi Ebukhoma ivaliwe", - "activityKitDisabledBody": "Vula Imisebenzi Ebukhoma ku-Izilungiselelo ukuze ubone Ama-agent asebenzayo Esikrinini Esikhiyiwe." + "reconnecting": "Ixhuma kabusha" } } From ecc258fed8c2f0d37e2e08c5c461cc11ef72d66a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 3 Sep 2026 13:35:47 +0200 Subject: [PATCH 7/8] fix(mobile): keep the glanceable surface blank after a lost org The terminal-blank epoch gates only the publisher that existed at the blank. A token refresh or a remount builds a new publisher, which captures the new epoch and republishes the revoked org's cached counts. Add a lost-org latch that every publisher reads on each emit. The org fence sets it on a confirmed lost org and clears it only when a successful organization list holds the selection. Claude-Session: https://claude.ai/code/session_01MHf6WEQu5qVG8yYN7g2G3Y --- .../mobile/src/lib/glanceable/cleanup.test.ts | 50 ++++++++++++++++++- apps/mobile/src/lib/glanceable/cleanup.ts | 33 ++++++++++-- apps/mobile/src/lib/glanceable/mount.tsx | 3 +- apps/mobile/src/lib/glanceable/org-fence.ts | 3 ++ apps/mobile/src/lib/glanceable/publisher.ts | 10 +++- 5 files changed, 90 insertions(+), 9 deletions(-) diff --git a/apps/mobile/src/lib/glanceable/cleanup.test.ts b/apps/mobile/src/lib/glanceable/cleanup.test.ts index 6f13311da0..b96ba59193 100644 --- a/apps/mobile/src/lib/glanceable/cleanup.test.ts +++ b/apps/mobile/src/lib/glanceable/cleanup.test.ts @@ -3,11 +3,15 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; import { type GlanceableAgentsSnapshot } from '@kilocode/app-shared/glanceable-agents-snapshot'; import { + confirmGlanceableOrgMembership, + getTerminalBlankEpoch, + isGlanceableOrgLost, planOrgFenceAction, republishLastSnapshotStale, writePrivacySnapshotAndEnd, writeSignedOutSnapshotAndEnd, } from './cleanup'; +import { GlanceablePublisher } from './publisher'; import { _resetGlanceablePersistForTests, _setLastGlanceableSnapshotForTests, @@ -41,6 +45,8 @@ function makeSink() { return { sink, calls }; } +const PUB_CTX = { userId: 'u1', organizationId: 'revoked-org' }; + function lastSnapshot(calls: SinkCall[]): GlanceableAgentsSnapshot { const found = [...calls].toReversed().find(call => call.type === 'publish'); if (found === undefined) { @@ -51,6 +57,8 @@ function lastSnapshot(calls: SinkCall[]): GlanceableAgentsSnapshot { afterEach(() => { _resetGlanceablePersistForTests(); + // The lost-org latch is module state: release it so it cannot leak forward. + confirmGlanceableOrgMembership(); vi.useRealTimers(); }); @@ -82,6 +90,44 @@ describe('cleanup', () => { } }); + it('keeps a rebuilt publisher silent after a lost org until membership returns', () => { + const { sink, calls } = makeSink(); + registerGlanceableSink(sink); + const options = { + sinks: [sink], + terminalBlankEpoch: getTerminalBlankEpoch, + orgLost: isGlanceableOrgLost, + }; + const publisher = new GlanceablePublisher(options); + let rebuilt: GlanceablePublisher | null = null; + try { + publisher.handleSessions([{ status: 'busy' }], PUB_CTX); + expect(calls.filter(call => call.type === 'startOrUpdate')).toHaveLength(1); + + writePrivacySnapshotAndEnd(); + expect(lastSnapshot(calls).status).toBe('privacy'); + const afterBlank = calls.filter(call => call.type === 'publish').length; + + // A token refresh rebuilds the publisher: it captures the new epoch, so + // only the latch stops it republishing the revoked org's counts. + rebuilt = new GlanceablePublisher(options); + rebuilt.handleSessions([{ status: 'busy' }], PUB_CTX); + expect(calls.filter(call => call.type === 'publish')).toHaveLength(afterBlank); + expect(calls.filter(call => call.type === 'startOrUpdate')).toHaveLength(1); + expect(lastSnapshot(calls).status).toBe('privacy'); + + // A successful org list that holds the selection releases the latch. + confirmGlanceableOrgMembership(); + rebuilt.handleSessions([{ status: 'busy' }], PUB_CTX); + expect(calls.filter(call => call.type === 'startOrUpdate')).toHaveLength(2); + expect(lastSnapshot(calls).status).toBe('happy'); + } finally { + unregisterGlanceableSink(sink); + publisher.dispose(); + rebuilt?.dispose(); + } + }); + it('blanks to privacy only after a successful list misses the selection', () => { expect( planOrgFenceAction({ @@ -106,10 +152,10 @@ describe('cleanup', () => { isLoading: false, isError: false, }) - ).toBe('none'); + ).toBe('confirmed'); expect( planOrgFenceAction({ organizationId: null, orgs: [], isLoading: false, isError: false }) - ).toBe('none'); + ).toBe('confirmed'); // An offline or not-yet-fetched list (orgs undefined) is not a lost org. expect( planOrgFenceAction({ diff --git a/apps/mobile/src/lib/glanceable/cleanup.ts b/apps/mobile/src/lib/glanceable/cleanup.ts index ded58ef91b..0a86b3a4f0 100644 --- a/apps/mobile/src/lib/glanceable/cleanup.ts +++ b/apps/mobile/src/lib/glanceable/cleanup.ts @@ -18,6 +18,17 @@ export function getTerminalBlankEpoch(): number { return terminalBlankEpoch; } +// The epoch alone only gates publishers that already existed at the blank. A +// confirmed lost org outlives them: a token refresh or a remount builds a new +// publisher that would republish the revoked org's cached counts. This latch +// blocks every publisher until a successful org list confirms membership. +let orgMembershipLost = false; + +/** True while a confirmed lost org blocks publication. */ +export function isGlanceableOrgLost(): boolean { + return orgMembershipLost; +} + /** * Terminal blanking: signed-out and privacy states are written to every sink * (publish) and then every sink ends immediately. The 8 s terminal window is @@ -32,9 +43,13 @@ export type GlanceableOrgFenceState = { isError: boolean; }; -export type GlanceableOrgFenceAction = 'privacy' | 'stale' | 'none'; +export type GlanceableOrgFenceAction = 'privacy' | 'stale' | 'confirmed' | 'none'; -/** Pure org-fence decision: lost org only after a successful list misses the selection. */ +/** + * Pure org-fence decision: lost org only after a successful list misses the + * selection, `confirmed` only after a successful list holds it. A loading, + * errored, or absent list is `none` or `stale`, never a confirmation. + */ export function planOrgFenceAction(state: GlanceableOrgFenceState): GlanceableOrgFenceAction { if (state.isLoading) { return 'none'; @@ -42,14 +57,16 @@ export function planOrgFenceAction(state: GlanceableOrgFenceState): GlanceableOr if (state.isError) { return 'stale'; } + if (state.orgs === undefined) { + return 'none'; + } if ( state.organizationId !== null && - state.orgs !== undefined && !state.orgs.some(entry => entry.organizationId === state.organizationId) ) { return 'privacy'; } - return 'none'; + return 'confirmed'; } function buildTerminalSnapshot(status: 'signed_out' | 'privacy'): GlanceableAgentsSnapshot { @@ -94,11 +111,17 @@ export function writeSignedOutSnapshotAndEnd(): void { writeTerminalAndEnd('signed_out'); } -/** Blank on org switch or confirmed lost org. */ +/** Blank on org switch or confirmed lost org, and latch publication off. */ export function writePrivacySnapshotAndEnd(): void { + orgMembershipLost = true; writeTerminalAndEnd('privacy'); } +/** A successful org list holds the selection: release the lost-org latch. */ +export function confirmGlanceableOrgMembership(): void { + orgMembershipLost = false; +} + /** * Republish the last snapshot as stale until its deadline, then expired. Used * by the org fence when the org list errors: stale, not lost-org. diff --git a/apps/mobile/src/lib/glanceable/mount.tsx b/apps/mobile/src/lib/glanceable/mount.tsx index f50680f6a9..b08cdd20aa 100644 --- a/apps/mobile/src/lib/glanceable/mount.tsx +++ b/apps/mobile/src/lib/glanceable/mount.tsx @@ -15,7 +15,7 @@ import { persistGlanceableSink, restorePersistedGlanceable, } from './persist'; -import { getTerminalBlankEpoch } from './cleanup'; +import { getTerminalBlankEpoch, isGlanceableOrgLost } from './cleanup'; import { GlanceablePublisher } from './publisher'; import { getGlanceableSinks, registerGlanceableSink } from './sink-registry'; @@ -68,6 +68,7 @@ export function GlanceablePublisherMount(): null { sinks: getGlanceableSinks(), initial: getLastGlanceableSnapshot(), terminalBlankEpoch: getTerminalBlankEpoch, + orgLost: isGlanceableOrgLost, }); const ctx = { userId, organizationId }; diff --git a/apps/mobile/src/lib/glanceable/org-fence.ts b/apps/mobile/src/lib/glanceable/org-fence.ts index 960a5562b1..0ef5c39143 100644 --- a/apps/mobile/src/lib/glanceable/org-fence.ts +++ b/apps/mobile/src/lib/glanceable/org-fence.ts @@ -6,6 +6,7 @@ import { useOrganization } from '@/lib/organization-context'; import { useTRPC } from '@/lib/trpc'; import { + confirmGlanceableOrgMembership, planOrgFenceAction, republishLastSnapshotStale, writePrivacySnapshotAndEnd, @@ -35,6 +36,8 @@ export function useGlanceableOrgFence(): void { const action = planOrgFenceAction({ organizationId, orgs, isLoading, isError }); if (action === 'privacy') { writePrivacySnapshotAndEnd(); + } else if (action === 'confirmed') { + confirmGlanceableOrgMembership(); } else if (action === 'stale' && getLastGlanceableSnapshot() !== null) { republishLastSnapshotStale(); } diff --git a/apps/mobile/src/lib/glanceable/publisher.ts b/apps/mobile/src/lib/glanceable/publisher.ts index e74f241e72..4a0471373f 100644 --- a/apps/mobile/src/lib/glanceable/publisher.ts +++ b/apps/mobile/src/lib/glanceable/publisher.ts @@ -36,6 +36,12 @@ export type GlanceablePublisherOptions = { * success after a signed-out or privacy blank cannot republish or restart. */ terminalBlankEpoch?: () => number; + /** + * Confirmed-lost-org latch reader (see cleanup). Read on every emit, not at + * construction, so a publisher rebuilt by a token refresh or a remount stays + * silent until a successful org list confirms membership again. + */ + orgLost?: () => boolean; }; type TimerHandle = ReturnType; @@ -75,6 +81,7 @@ export class GlanceablePublisher { private readonly terminalMs: number; private readonly terminalBlankEpoch: () => number; private readonly blankEpochAtStart: number; + private readonly orgLost: () => boolean; private current: GlanceableAgentsSnapshot | null; private activityStarted: boolean; private coalesceTimer: TimerHandle | null = null; @@ -91,6 +98,7 @@ export class GlanceablePublisher { this.terminalMs = options.terminalMs ?? GLANCEABLE_TERMINAL_MS; this.terminalBlankEpoch = options.terminalBlankEpoch ?? (() => 0); this.blankEpochAtStart = this.terminalBlankEpoch(); + this.orgLost = options.orgLost ?? (() => false); this.current = options.initial ?? null; this.activityStarted = false; } @@ -203,7 +211,7 @@ export class GlanceablePublisher { } private isGated(): boolean { - return this.terminalBlankEpoch() !== this.blankEpochAtStart; + return this.terminalBlankEpoch() !== this.blankEpochAtStart || this.orgLost(); } private emit(snapshot: GlanceableAgentsSnapshot, ctx: GlanceableSinkContext): void { From cb51e0ffc88de4088a7d1ee5e687292a41900f39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 3 Sep 2026 13:55:05 +0200 Subject: [PATCH 8/8] fix(mobile): latch only a confirmed lost org, not an org switch `organization-context` blanks to privacy on every intentional org switch. Latching there kept the rebuilt publisher silent for the new org until the next poll, because the fence clears the latch after the publisher's effect has already run. Split the two intents: `writeLostOrgSnapshotAndEnd` latches, and `writePrivacySnapshotAndEnd` only blanks. Claude-Session: https://claude.ai/code/session_01MHf6WEQu5qVG8yYN7g2G3Y --- .../mobile/src/lib/glanceable/cleanup.test.ts | 24 ++++++++++++++++++- apps/mobile/src/lib/glanceable/cleanup.ts | 7 +++++- apps/mobile/src/lib/glanceable/org-fence.ts | 4 ++-- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/apps/mobile/src/lib/glanceable/cleanup.test.ts b/apps/mobile/src/lib/glanceable/cleanup.test.ts index b96ba59193..eb6bb4bb5d 100644 --- a/apps/mobile/src/lib/glanceable/cleanup.test.ts +++ b/apps/mobile/src/lib/glanceable/cleanup.test.ts @@ -8,6 +8,7 @@ import { isGlanceableOrgLost, planOrgFenceAction, republishLastSnapshotStale, + writeLostOrgSnapshotAndEnd, writePrivacySnapshotAndEnd, writeSignedOutSnapshotAndEnd, } from './cleanup'; @@ -90,6 +91,27 @@ describe('cleanup', () => { } }); + it('does not latch publication off for an intentional org switch', () => { + const { sink, calls } = makeSink(); + registerGlanceableSink(sink); + writePrivacySnapshotAndEnd(); + expect(isGlanceableOrgLost()).toBe(false); + // The org change rebuilds the publisher, which must emit for the new org + // without waiting for the fence to confirm membership. + const publisher = new GlanceablePublisher({ + sinks: [sink], + terminalBlankEpoch: getTerminalBlankEpoch, + orgLost: isGlanceableOrgLost, + }); + try { + publisher.handleSessions([{ status: 'busy' }], PUB_CTX); + expect(lastSnapshot(calls).status).toBe('happy'); + } finally { + unregisterGlanceableSink(sink); + publisher.dispose(); + } + }); + it('keeps a rebuilt publisher silent after a lost org until membership returns', () => { const { sink, calls } = makeSink(); registerGlanceableSink(sink); @@ -104,7 +126,7 @@ describe('cleanup', () => { publisher.handleSessions([{ status: 'busy' }], PUB_CTX); expect(calls.filter(call => call.type === 'startOrUpdate')).toHaveLength(1); - writePrivacySnapshotAndEnd(); + writeLostOrgSnapshotAndEnd(); expect(lastSnapshot(calls).status).toBe('privacy'); const afterBlank = calls.filter(call => call.type === 'publish').length; diff --git a/apps/mobile/src/lib/glanceable/cleanup.ts b/apps/mobile/src/lib/glanceable/cleanup.ts index 0a86b3a4f0..ee168776d1 100644 --- a/apps/mobile/src/lib/glanceable/cleanup.ts +++ b/apps/mobile/src/lib/glanceable/cleanup.ts @@ -111,8 +111,13 @@ export function writeSignedOutSnapshotAndEnd(): void { writeTerminalAndEnd('signed_out'); } -/** Blank on org switch or confirmed lost org, and latch publication off. */ +/** Blank on an intentional org switch. The next org may publish at once. */ export function writePrivacySnapshotAndEnd(): void { + writeTerminalAndEnd('privacy'); +} + +/** Blank on a confirmed lost org, and latch publication off until it returns. */ +export function writeLostOrgSnapshotAndEnd(): void { orgMembershipLost = true; writeTerminalAndEnd('privacy'); } diff --git a/apps/mobile/src/lib/glanceable/org-fence.ts b/apps/mobile/src/lib/glanceable/org-fence.ts index 0ef5c39143..ab92bc9dc3 100644 --- a/apps/mobile/src/lib/glanceable/org-fence.ts +++ b/apps/mobile/src/lib/glanceable/org-fence.ts @@ -9,7 +9,7 @@ import { confirmGlanceableOrgMembership, planOrgFenceAction, republishLastSnapshotStale, - writePrivacySnapshotAndEnd, + writeLostOrgSnapshotAndEnd, } from './cleanup'; import { getLastGlanceableSnapshot } from './persist'; @@ -35,7 +35,7 @@ export function useGlanceableOrgFence(): void { useEffect(() => { const action = planOrgFenceAction({ organizationId, orgs, isLoading, isError }); if (action === 'privacy') { - writePrivacySnapshotAndEnd(); + writeLostOrgSnapshotAndEnd(); } else if (action === 'confirmed') { confirmGlanceableOrgMembership(); } else if (action === 'stale' && getLastGlanceableSnapshot() !== null) {