@athenaintel/react 0.55.0 → 0.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -1
- package/dist/agent-runtime/src/answer-label.cjs +8 -2
- package/dist/agent-runtime/src/answer-label.cjs.map +1 -1
- package/dist/agent-runtime/src/answer-label.d.ts +10 -4
- package/dist/agent-runtime/src/answer-label.js +8 -2
- package/dist/agent-runtime/src/answer-label.js.map +1 -1
- package/dist/agent-runtime/src/deep-agent-transport-recipe.cjs +6 -3
- package/dist/agent-runtime/src/deep-agent-transport-recipe.cjs.map +1 -1
- package/dist/agent-runtime/src/deep-agent-transport-recipe.js +6 -3
- package/dist/agent-runtime/src/deep-agent-transport-recipe.js.map +1 -1
- package/dist/agent-runtime/src/edit-command.cjs +40 -19
- package/dist/agent-runtime/src/edit-command.cjs.map +1 -1
- package/dist/agent-runtime/src/edit-command.d.ts +2 -0
- package/dist/agent-runtime/src/edit-command.js +41 -20
- package/dist/agent-runtime/src/edit-command.js.map +1 -1
- package/dist/agent-runtime/src/generated/runconfig-contract.cjs +8 -0
- package/dist/agent-runtime/src/generated/runconfig-contract.cjs.map +1 -1
- package/dist/agent-runtime/src/generated/runconfig-contract.d.ts +9 -0
- package/dist/agent-runtime/src/generated/runconfig-contract.js +8 -0
- package/dist/agent-runtime/src/generated/runconfig-contract.js.map +1 -1
- package/dist/agent-runtime/src/history-store.cjs +426 -0
- package/dist/agent-runtime/src/history-store.cjs.map +1 -0
- package/dist/agent-runtime/src/history-store.d.ts +282 -0
- package/dist/agent-runtime/src/history-store.js +426 -0
- package/dist/agent-runtime/src/history-store.js.map +1 -0
- package/dist/agent-runtime/src/inbound-channel.cjs +96 -0
- package/dist/agent-runtime/src/inbound-channel.cjs.map +1 -0
- package/dist/agent-runtime/src/inbound-channel.d.ts +75 -0
- package/dist/agent-runtime/src/inbound-channel.js +96 -0
- package/dist/agent-runtime/src/inbound-channel.js.map +1 -0
- package/dist/agent-runtime/src/index.d.ts +6 -5
- package/dist/agent-runtime/src/interrupts.cjs +45 -3
- package/dist/agent-runtime/src/interrupts.cjs.map +1 -1
- package/dist/agent-runtime/src/interrupts.d.ts +51 -4
- package/dist/agent-runtime/src/interrupts.js +46 -4
- package/dist/agent-runtime/src/interrupts.js.map +1 -1
- package/dist/agent-runtime/src/message-window.cjs +43 -5
- package/dist/agent-runtime/src/message-window.cjs.map +1 -1
- package/dist/agent-runtime/src/message-window.d.ts +26 -2
- package/dist/agent-runtime/src/message-window.js +44 -6
- package/dist/agent-runtime/src/message-window.js.map +1 -1
- package/dist/agent-runtime/src/offline-status.cjs.map +1 -1
- package/dist/agent-runtime/src/offline-status.d.ts +110 -4
- package/dist/agent-runtime/src/offline-status.js.map +1 -1
- package/dist/agent-runtime/src/runtime.d.ts +1 -0
- package/dist/agent-runtime/src/thread-handoff-flow-machine.d.ts +8 -8
- package/dist/agent-runtime/src/tool-metadata.cjs +16 -0
- package/dist/agent-runtime/src/tool-metadata.cjs.map +1 -1
- package/dist/agent-runtime/src/tool-metadata.js +16 -0
- package/dist/agent-runtime/src/tool-metadata.js.map +1 -1
- package/dist/agent-runtime/src/use-deep-agent-thread.cjs +8 -16
- package/dist/agent-runtime/src/use-deep-agent-thread.cjs.map +1 -1
- package/dist/agent-runtime/src/use-deep-agent-thread.d.ts +1 -9
- package/dist/agent-runtime/src/use-deep-agent-thread.js +9 -17
- package/dist/agent-runtime/src/use-deep-agent-thread.js.map +1 -1
- package/dist/agent-runtime/src/use-deep-agent-window-sync.cjs +86 -79
- package/dist/agent-runtime/src/use-deep-agent-window-sync.cjs.map +1 -1
- package/dist/agent-runtime/src/use-deep-agent-window-sync.d.ts +2 -27
- package/dist/agent-runtime/src/use-deep-agent-window-sync.js +89 -82
- package/dist/agent-runtime/src/use-deep-agent-window-sync.js.map +1 -1
- package/dist/auth/AthenaSSOProvider.cjs +54 -12
- package/dist/auth/AthenaSSOProvider.cjs.map +1 -1
- package/dist/auth/AthenaSSOProvider.d.ts +23 -2
- package/dist/auth/AthenaSSOProvider.js +54 -12
- package/dist/auth/AthenaSSOProvider.js.map +1 -1
- package/dist/auth/index.cjs +2 -0
- package/dist/auth/index.cjs.map +1 -1
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.js +2 -0
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/sign-back-in.cjs +141 -0
- package/dist/auth/sign-back-in.cjs.map +1 -0
- package/dist/auth/sign-back-in.d.ts +117 -0
- package/dist/auth/sign-back-in.js +141 -0
- package/dist/auth/sign-back-in.js.map +1 -0
- package/dist/chat/AthenaChat.cjs +7 -2
- package/dist/chat/AthenaChat.cjs.map +1 -1
- package/dist/chat/AthenaChat.js +7 -2
- package/dist/chat/AthenaChat.js.map +1 -1
- package/dist/chat/StatewireApprovalCard.cjs +6 -2
- package/dist/chat/StatewireApprovalCard.cjs.map +1 -1
- package/dist/chat/StatewireApprovalCard.d.ts +8 -1
- package/dist/chat/StatewireApprovalCard.js +7 -3
- package/dist/chat/StatewireApprovalCard.js.map +1 -1
- package/dist/chat/super-grouping/CollapsibleGroup.cjs +1 -1
- package/dist/chat/super-grouping/CollapsibleGroup.cjs.map +1 -1
- package/dist/chat/super-grouping/CollapsibleGroup.d.ts +6 -5
- package/dist/chat/super-grouping/CollapsibleGroup.js +1 -1
- package/dist/chat/super-grouping/CollapsibleGroup.js.map +1 -1
- package/dist/chat/super-grouping/earlier-steps-button.cjs +5 -6
- package/dist/chat/super-grouping/earlier-steps-button.cjs.map +1 -1
- package/dist/chat/super-grouping/earlier-steps-button.d.ts +6 -6
- package/dist/chat/super-grouping/earlier-steps-button.js +5 -6
- package/dist/chat/super-grouping/earlier-steps-button.js.map +1 -1
- package/dist/chat/super-grouping/toolkit-map.cjs +1 -0
- package/dist/chat/super-grouping/toolkit-map.cjs.map +1 -1
- package/dist/chat/super-grouping/toolkit-map.js +1 -0
- package/dist/chat/super-grouping/toolkit-map.js.map +1 -1
- package/dist/chat/super-grouping/use-turn-steps.cjs +3 -3
- package/dist/chat/super-grouping/use-turn-steps.js +1 -1
- package/dist/chat-ui/index.cjs +4 -0
- package/dist/chat-ui/index.cjs.map +1 -1
- package/dist/chat-ui/index.d.ts +1 -0
- package/dist/chat-ui/index.js +5 -1
- package/dist/chat-ui/index.js.map +1 -1
- package/dist/chat-ui/messages/inbound-channel-message.cjs +219 -0
- package/dist/chat-ui/messages/inbound-channel-message.cjs.map +1 -0
- package/dist/chat-ui/messages/inbound-channel-message.d.ts +60 -0
- package/dist/chat-ui/messages/inbound-channel-message.js +219 -0
- package/dist/chat-ui/messages/inbound-channel-message.js.map +1 -0
- package/dist/chat-ui/messages/inbound-channel-reader.cjs +184 -0
- package/dist/chat-ui/messages/inbound-channel-reader.cjs.map +1 -0
- package/dist/chat-ui/messages/inbound-channel-reader.d.ts +65 -0
- package/dist/chat-ui/messages/inbound-channel-reader.js +184 -0
- package/dist/chat-ui/messages/inbound-channel-reader.js.map +1 -0
- package/dist/chat-ui/messages/message-components.cjs +18 -14
- package/dist/chat-ui/messages/message-components.cjs.map +1 -1
- package/dist/chat-ui/messages/message-components.d.ts +7 -0
- package/dist/chat-ui/messages/message-components.js +19 -15
- package/dist/chat-ui/messages/message-components.js.map +1 -1
- package/dist/chat-ui/thread/statewire-history-edge.cjs +0 -4
- package/dist/chat-ui/thread/statewire-history-edge.cjs.map +1 -1
- package/dist/chat-ui/thread/statewire-history-edge.d.ts +5 -7
- package/dist/chat-ui/thread/statewire-history-edge.js +1 -5
- package/dist/chat-ui/thread/statewire-history-edge.js.map +1 -1
- package/dist/composer/sign-back-in-draft.cjs +62 -0
- package/dist/composer/sign-back-in-draft.cjs.map +1 -0
- package/dist/composer/sign-back-in-draft.d.ts +34 -0
- package/dist/composer/sign-back-in-draft.js +62 -0
- package/dist/composer/sign-back-in-draft.js.map +1 -0
- package/dist/diagnostics/athena-diagnostics.cjs.map +1 -1
- package/dist/diagnostics/athena-diagnostics.d.ts +6 -1
- package/dist/diagnostics/athena-diagnostics.js.map +1 -1
- package/dist/index.cjs +8 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/provider/AthenaProvider.cjs +60 -14
- package/dist/provider/AthenaProvider.cjs.map +1 -1
- package/dist/provider/AthenaProvider.js +60 -14
- package/dist/provider/AthenaProvider.js.map +1 -1
- package/dist/provider/statewire-window-context.cjs.map +1 -1
- package/dist/provider/statewire-window-context.d.ts +8 -8
- package/dist/provider/statewire-window-context.js.map +1 -1
- package/dist/react/package.json.cjs +1 -1
- package/dist/react/package.json.js +1 -1
- package/dist/runtime/auth.cjs +11 -0
- package/dist/runtime/auth.cjs.map +1 -1
- package/dist/runtime/auth.d.ts +18 -1
- package/dist/runtime/auth.js +11 -0
- package/dist/runtime/auth.js.map +1 -1
- package/dist/runtime/useAthenaStatewireRuntime.cjs +0 -2
- package/dist/runtime/useAthenaStatewireRuntime.cjs.map +1 -1
- package/dist/runtime/useAthenaStatewireRuntime.d.ts +5 -5
- package/dist/runtime/useAthenaStatewireRuntime.js +0 -2
- package/dist/runtime/useAthenaStatewireRuntime.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/dist/agent-runtime/src/history-pages.cjs +0 -279
- package/dist/agent-runtime/src/history-pages.cjs.map +0 -1
- package/dist/agent-runtime/src/history-pages.d.ts +0 -252
- package/dist/agent-runtime/src/history-pages.js +0 -279
- package/dist/agent-runtime/src/history-pages.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"offline-status.cjs","sources":["../../../../agent-runtime/src/offline-status.ts"],"sourcesContent":["/**\n * Athena's offline language: the one set of states and words every app uses to\n * say where the person stands when the network, or Athena, is out of reach —\n * Olympus, the desktop app, the phone app, the Chrome extension and the Office\n * add-in.\n *\n * The rule the words follow: say what still works before what doesn't. Work\n * done while Athena can't be reached is kept on the device and syncs when\n * Athena is back, so every message leads with that promise. Nothing here is\n * shown while Athena answers and nothing waits; the \"back\" messages are brief.\n *\n * Pure strings and small functions, no UI library: each app draws them its own\n * way (a banner, a header chip, a tooltip, a sheet). Reachability itself is\n * each app's own (the browser's or OS's online flag AND agora answering its\n * health read); this module only names the result.\n */\n\n/** Where the app stands with Athena. */\nexport type ConnectionState =\n /** Athena answers. Nothing is shown. */\n | 'online'\n /** The device has no network. */\n | 'offline'\n /** The network is up but Athena doesn't answer. */\n | 'unreachable'\n /** Athena answers again and what waited is being sent. */\n | 'reconnecting'\n /** Just back, and everything that waited was sent. Shown briefly. */\n | 'synced';\n\n/** Where one document's edits stand, for the chip in its header. */\nexport type DocumentSyncState =\n /** In Athena. */\n | 'saved'\n /** Being sent to Athena. */\n | 'saving'\n /** Kept on this device while Athena can't be reached; syncs when it is back. */\n | 'on-device'\n /** Kept only while this tab or app stays open (no device storage); syncs when Athena is back. */\n | 'in-memory'\n /** Sending what was kept on this device. */\n | 'syncing'\n /** Some changes overlap with Athena's version: the person chooses. */\n | 'needs-review'\n /** The document can no longer take these edits (access or state changed): kept, not sent. */\n | 'view-only'\n /**\n * Athena answers but isn't taking this document's changes for now: it holds the document while it\n * repairs it, or hasn't confirmed the latest changes for longer than usual (`SavingDelayCause`\n * says which). Kept, and saved once Athena takes them. Never said while Athena can't be reached:\n * that is `on-device` or `in-memory`.\n */\n | 'delayed';\n\n/** Why a document's saving is delayed while Athena answers (`DocumentSyncState` `delayed`). */\nexport type SavingDelayCause =\n /** Athena holds the document while it repairs it, and takes no changes to it until then. */\n | 'repairing'\n /** Athena hasn't confirmed the latest changes for longer than usual. */\n | 'unconfirmed';\n\n/** How a sentence names the device the person is on. */\nexport type DeviceNoun =\n | 'this browser'\n | 'this Mac'\n | 'this computer'\n | 'this phone'\n | 'this tablet'\n | 'this device';\n\n/**\n * What the app really keeps of work done while Athena can't be reached, so the\n * words never promise more: `device` (stored on the device, survives closing),\n * `memory` (kept only while this tab or app stays open: a tenant that turns\n * device copies off, or storage the app can't open), `none` (a surface that\n * keeps no work offline at all, like the Chrome side panel).\n */\nexport type OfflineKeeping = 'device' | 'memory' | 'none';\n\n/** A title and the one line under it (a banner, a sheet, a tooltip's heading). */\nexport interface StatusCopy {\n title: string;\n detail: string;\n}\n\n/** A short label for a chip or a disabled control, and its one-line explanation. */\nexport interface LabelCopy {\n label: string;\n detail: string;\n}\n\nfunction plural(count: number, one: string, many: string): string {\n return `${count} ${count === 1 ? one : many}`;\n}\n\n/** What a tab or an app is called when work lasts only while it stays open. */\nfunction openContainer(device: DeviceNoun): string {\n return device === 'this browser' ? 'this tab' : 'the app';\n}\n\n/** The promise after \"You're offline\" / \"Can't reach Athena\", honest to what is kept. */\nfunction keepingPromise({\n keeping,\n device,\n when,\n}: {\n keeping: OfflineKeeping;\n device: DeviceNoun;\n when: string;\n}): string {\n switch (keeping) {\n case 'device':\n return `You can keep working. Changes are saved on ${device} and sync ${when}.`;\n case 'memory':\n return `You can keep working, but changes are kept only until you close ${openContainer(device)}. They sync ${when}.`;\n case 'none':\n return `You’re still signed in. This reconnects ${when}.`;\n }\n}\n\n/**\n * The app-wide status line, or `null` while Athena answers and nothing waits.\n * `waiting` counts changes not in Athena yet: while `reconnecting` they are\n * being sent; while `online` they wait for something else (a copy that sends\n * when it is next opened), and the line stays until they are gone.\n * `keeping` is what the app really keeps offline (default `device`). A surface\n * that keeps nothing (`none`) has nothing to sync, so its way back is only\n * \"Back online\", never \"Syncing…\" or \"All changes synced\".\n */\nexport function connectionCopy({\n state,\n device,\n waiting = 0,\n keeping = 'device',\n}: {\n state: ConnectionState;\n device: DeviceNoun;\n waiting?: number;\n keeping?: OfflineKeeping;\n}): StatusCopy | null {\n switch (state) {\n case 'online':\n return waiting > 0\n ? {\n title: 'Not synced yet',\n detail: `${plural(waiting, 'change', 'changes')} on ${device} ${waiting === 1 ? 'hasn’t' : 'haven’t'} synced yet.`,\n }\n : null;\n case 'offline':\n return {\n title: 'You’re offline',\n detail: keepingPromise({ keeping, device, when: 'when you’re back online' }),\n };\n case 'unreachable':\n return {\n title: 'Can’t reach Athena',\n detail: keepingPromise({ keeping, device, when: 'when Athena is back' }),\n };\n case 'reconnecting':\n if (keeping === 'none') return { title: 'Back online', detail: '' };\n return {\n title: 'Back online',\n detail:\n waiting > 0 ? `Syncing ${plural(waiting, 'change', 'changes')}…` : 'Syncing…',\n };\n case 'synced':\n return keeping === 'none'\n ? { title: 'Back online', detail: '' }\n : { title: 'All changes synced', detail: '' };\n }\n}\n\n/**\n * The line under \"Saving delayed\": why saving waits, and what is kept meanwhile, never promising\n * more than the app keeps.\n */\nfunction savingDelayedDetail({\n delay,\n keeping,\n device,\n}: {\n delay: SavingDelayCause;\n keeping: Exclude<OfflineKeeping, 'none'>;\n device: DeviceNoun;\n}): string {\n if (delay === 'repairing') {\n return keeping === 'memory'\n ? `You can keep working, but your changes are kept only until you close ${openContainer(device)}. They save once Athena finishes repairing this document.`\n : `You can keep working. Your changes are kept on ${device} and save once Athena finishes repairing this document.`;\n }\n return keeping === 'memory'\n ? `Athena is taking longer than usual to save your latest changes. They’re kept only until you close ${openContainer(device)}, so keep it open.`\n : `Athena is taking longer than usual to save your latest changes. They’re kept on ${device} in the meantime.`;\n}\n\n/**\n * What `on-device` says. The desktop app on a Mac speaks as Word, Excel and PowerPoint for Mac do\n * of a cloud document whose changes wait on the Mac (Office's own title-bar words): \"Upload\n * Pending\", and \"Upload Pending (Offline)\" while Athena can't be reached. Every other app says the\n * changes are saved on its device: a browser has no such words (Word for the web can't edit\n * offline), nor does the phone.\n */\nfunction onDeviceCopy({ device, reachable }: { device: DeviceNoun; reachable: boolean }): LabelCopy {\n if (device === 'this Mac') {\n return {\n label: reachable ? 'Upload Pending' : 'Upload Pending (Offline)',\n detail: 'Upload pending. Your changes sync when Athena is back.',\n };\n }\n return {\n label: `Saved on ${device}`,\n detail: `Saved on ${device}. Your changes sync when Athena is back.`,\n };\n}\n\n/**\n * The chip in a document's header, and the line it explains itself with. For `delayed`, `delay`\n * says why (default `unconfirmed`) and `keeping` whether the changes are kept on the device or only\n * while the tab or app stays open (default `device`); every other state ignores both. `reachable`\n * (default `true`) is whether Athena can be reached, which only the Mac's `on-device` words read.\n */\nexport function documentSyncCopy({\n state,\n device,\n delay = 'unconfirmed',\n keeping = 'device',\n reachable = true,\n}: {\n state: DocumentSyncState;\n device: DeviceNoun;\n delay?: SavingDelayCause;\n keeping?: Exclude<OfflineKeeping, 'none'>;\n reachable?: boolean;\n}): LabelCopy {\n switch (state) {\n case 'saved':\n return { label: 'Saved', detail: 'Saved to Athena.' };\n case 'saving':\n return { label: 'Saving…', detail: 'Saving to Athena.' };\n case 'on-device':\n return onDeviceCopy({ device, reachable });\n case 'in-memory':\n return {\n label: 'Not saved yet',\n detail: `Your changes are kept only until you close ${openContainer(device)}, and save when Athena is back.`,\n };\n case 'syncing':\n return { label: 'Syncing…', detail: `Sending the changes saved on ${device}.` };\n case 'needs-review':\n return {\n label: 'Review changes',\n detail: 'Some of your changes overlap with changes made in Athena. Choose what to keep.',\n };\n case 'view-only':\n return {\n label: 'View only',\n detail: `You can’t edit this anymore. Your changes stay on ${device}.`,\n };\n case 'delayed':\n return { label: 'Saving delayed', detail: savingDelayedDetail({ delay, keeping, device }) };\n }\n}\n\n/**\n * How long a document's edits may wait unconfirmed on a connection that is up before its saving\n * reads as delayed (`delayed`, `unconfirmed`), and before its room asks Keryx why: the stall window\n * every editor shares (`ACK_STALL_MS` in `@athenaintel/asset-host/local-updates`, which Olympus's\n * `room-sync-store.test.ts` holds equal).\n */\nexport const UNCONFIRMED_EDITS_STALL_MS = 15_000;\n\n/**\n * Whether a document's saving is delayed, and why (`DocumentSyncState` `delayed`): only while Athena\n * answers and the document's room is connected; `repairing` when Keryx refuses the room's writes\n * (quarantined while it is repaired: said at once, before anything waits), `unconfirmed` once its\n * edits have stayed unconfirmed past {@link UNCONFIRMED_EDITS_STALL_MS}; `null` otherwise. Every\n * app's chip asks it after a hold and after edits kept without a connection, and ahead of syncing\n * and saving, so a room that refuses writes never spins and being offline is never a delay.\n */\nexport function savingDelayCause({\n reachable,\n connected,\n writesRefused,\n unconfirmedStalled,\n}: {\n /** Athena answers (the app's reachability, never the socket alone). */\n reachable: boolean;\n /** The document's room is connected. */\n connected: boolean;\n /** Keryx's document status says the room takes no writes for now. */\n writesRefused: boolean;\n /** Local edits have waited unconfirmed past the stall window on this connection. */\n unconfirmedStalled: boolean;\n}): SavingDelayCause | null {\n if (!reachable || !connected) return null;\n if (writesRefused) return 'repairing';\n return unconfirmedStalled ? 'unconfirmed' : null;\n}\n\n/**\n * What the person lost of a document while it was open, when the server checked its room's grant\n * again (Keryx's 4401, after a share changed): `edit`, they may still view it but no longer edit it;\n * `view`, they may no longer open it at all.\n */\nexport type AccessLoss = 'edit' | 'view';\n\n/** What a document says once the person's access changed while it was open. */\nexport interface AccessChangedCopy extends StatusCopy {\n /**\n * The label of the button that saves the person's version as a file of their own, with the\n * changes Athena never took, or `null` when nothing is offered: nothing of theirs waits, or the\n * device no longer serves the document (`view`).\n */\n saveCopy: string | null;\n}\n\n/**\n * What a document says once the person's access to it changed while it was open (its editor turns\n * read-only at once), in Word's own words (Word for Mac's message bar, `strman_a6d4`, and its\n * \"Permissions Have Changed\" notice, `strman_0a3e`). `keptChanges`: changes Athena never took wait\n * on the device, never sent; Word offers to save a copy of the file so nothing typed is lost. A\n * `view` loss offers nothing to save: the device stops serving a document the person may no longer\n * read, and their changes stay where they are kept.\n */\nexport function accessChangedCopy({\n loss,\n keptChanges,\n}: {\n loss: AccessLoss;\n keptChanges: boolean;\n}): AccessChangedCopy {\n if (loss === 'view') {\n return {\n title: 'Permissions Have Changed',\n detail:\n 'The permissions on this file were updated and you no longer have rights to view this file. If you previously made changes to this file, they may not be saved.',\n saveCopy: null,\n };\n }\n if (keptChanges) {\n return {\n title: 'Can’t save',\n detail:\n 'You no longer have the permission to edit this file. To keep your changes, save a copy of the file.',\n saveCopy: 'Save a Copy',\n };\n }\n return {\n title: 'Read Only',\n detail: 'You no longer have the permission to edit this file.',\n saveCopy: null,\n };\n}\n\n/** When something that needs Athena works again (the second half of `NEEDS_CONNECTION`'s line). */\nconst AVAILABLE_AGAIN = 'It’s available again when you’re back online.';\n\n/**\n * The reason on a control that needs Athena (share, run, upload, invite…),\n * shown disabled while Athena can't be reached rather than failing when used.\n */\nexport const NEEDS_CONNECTION: LabelCopy = {\n label: 'Needs a connection',\n detail: `This needs Athena. ${AVAILABLE_AGAIN}`,\n};\n\n/** What a composer was given to attach: a photo (from the phone's library or camera) or any file. */\nexport type AttachmentKind = 'photo' | 'file';\n\n/**\n * What a composer says when a photo or file is picked, pasted or dropped while\n * Athena can't be reached. It isn't attached: only Athena can take the upload.\n * In a chat whose messages wait to be sent meanwhile (`messagesWait`: the chat\n * keeps an outbox), the message itself still goes, so that comes first. Where\n * nothing waits (the Chrome side panel, a launcher that keeps its draft), it\n * says only when attaching works again, as `NEEDS_CONNECTION` does.\n */\nexport function attachmentNeedsConnection({\n kind,\n messagesWait,\n}: {\n kind: AttachmentKind;\n messagesWait: boolean;\n}): string {\n const attaching = `Attaching ${kind === 'photo' ? 'a photo' : 'a file'} needs Athena. ${AVAILABLE_AGAIN}`;\n return messagesWait\n ? `You can still send your message: it goes when Athena is back. ${attaching}`\n : attaching;\n}\n\n/** What a sent message carries besides its text that can't wait for Athena. */\nfunction heldExtras(withQuote: boolean): string {\n return withQuote ? 'an attachment or a quote' : 'an attachment';\n}\n\n/**\n * What a composer says when a message is sent while Athena can't be reached\n * with an attachment that never reached Athena (or, `withQuote`, a quote the\n * chat can't keep waiting). Its text could wait in the chat outbox; the\n * attachment can't, so the draft stays as it is.\n */\nexport function attachedMessageNeedsConnection({\n withQuote = false,\n}: {\n withQuote?: boolean;\n} = {}): string {\n return `Your text can wait and go when Athena is back, but ${heldExtras(withQuote)} can’t. Remove ${\n withQuote ? 'it' : 'the attachment'\n } to send the text.`;\n}\n\n/**\n * What a composer says when a message with an attachment (or, `withQuote`, a\n * quote) is sent while messages written when Athena couldn't be reached still\n * wait to go: those go first, and this one can't wait behind them.\n */\nexport function attachedMessageBehindWaiting({\n withQuote = false,\n}: {\n withQuote?: boolean;\n} = {}): string {\n return `Messages you wrote while Athena couldn’t be reached go first. A message with ${heldExtras(\n withQuote\n )} can’t wait behind them, so send it once they’ve gone.`;\n}\n\n/**\n * The reason on New Presentation while Athena can't be reached, in a workspace\n * whose new presentations start from its design system's template: a\n * presentation made offline starts blank, and only Athena can copy the\n * template, so none is made until it can.\n */\nexport const NEW_PRESENTATION_NEEDS_TEMPLATE: LabelCopy = {\n label: 'Needs a connection',\n detail:\n 'New presentations in this workspace start from its design template, which needs Athena. You can make one when you’re back online.',\n};\n\n/**\n * What a download of a spreadsheet made from an Excel Macro-Enabled Workbook\n * (`.xlsm`) says when it went without the macros because they come from Athena,\n * which can't be reached, and the device hasn't kept them (they travel with the\n * workbook and never run). The file is the workbook without them; downloading\n * it again with a connection keeps them.\n */\nexport function downloadedWithoutMacros({ device }: { device: DeviceNoun }): string {\n return `Downloaded without the spreadsheet’s macros: they come from Athena, which can’t be reached, and ${device} hasn’t kept them. Download it again when you’re back online to keep them.`;\n}\n\n/**\n * Save's question for a spreadsheet whose file is an Excel Macro-Enabled\n * Workbook (`.xlsm`) while its macros come from Athena, which can't be reached,\n * and the device hasn't kept them: the `.xlsm` is never written without them,\n * so the person may save an Excel Workbook (`.xlsx`) beside it, and Save writes\n * the `.xlsm` again once Athena answers.\n */\nexport function macrosNeedConnectionToSave({\n name,\n device,\n}: {\n name: string;\n device: DeviceNoun;\n}): StatusCopy {\n return {\n title: `Athena can’t save the macros in “${name}” right now.`,\n detail: `They come from Athena, which can’t be reached, and ${device} hasn’t kept them. Save it as an Excel Workbook (.xlsx) instead. “${name}” stays as it is, macros included, and Save writes it again when you’re back online.`,\n };\n}\n\n/**\n * What saving a version kept on the device as a new spreadsheet says when the\n * spreadsheet was made from an Excel Macro-Enabled Workbook (`.xlsm`) and\n * Athena can't be reached: the new spreadsheet is made all the same (on the\n * device, and it syncs when Athena is back), but without the macros, which only\n * Athena can keep with a new spreadsheet (they travel with the workbook and\n * never run). The kept version stays on the device, so saving it again with a\n * connection keeps them.\n */\nexport function savedAsNewWithoutMacros({ device }: { device: DeviceNoun }): string {\n return `The new spreadsheet was made without the macros: only Athena can keep them in a new spreadsheet, and it can’t be reached. The kept version stays on ${device}: save it as a new spreadsheet again when you’re back online to keep them.`;\n}\n\n/**\n * Whether an item opens without a connection on this device, for lists.\n * `canKeep` says whether opening it online would keep a copy here (the\n * deployment allows device copies and the item's type is one that is kept):\n * only then is the person told to open it once.\n */\nexport function offlineAvailabilityCopy({\n available,\n canKeep,\n device,\n}: {\n available: boolean;\n canKeep: boolean;\n device: DeviceNoun;\n}): LabelCopy {\n if (available) {\n return { label: 'Available offline', detail: `Opens without a connection on ${device}.` };\n }\n return canKeep\n ? {\n label: 'Not available offline',\n detail: `Open it once while online to keep it on ${device}.`,\n }\n : { label: 'Not available offline', detail: 'This opens only with a connection.' };\n}\n\n/**\n * The one-time explainer shown the first time the person goes offline, on a\n * surface that keeps work there (`keeping` `device`). A surface that keeps\n * nothing (`none`: the Chrome side panel, the Office task pane) shows no\n * explainer; its status line already says all that holds.\n */\nexport function firstOfflineExplainer({ device }: { device: DeviceNoun }): StatusCopy & {\n action: string;\n} {\n return {\n title: 'You can keep working offline',\n detail: `Documents you’ve opened recently keep working on ${device}. Your changes sync when you’re back online.`,\n action: 'Got it',\n };\n}\n\n/**\n * The summary heading of the one \"Waiting to sync\" list, from what waits on\n * the device; `null` when nothing does.\n */\nexport function waitingSummary({\n changes = 0,\n messages = 0,\n recordings = 0,\n}: {\n changes?: number;\n messages?: number;\n recordings?: number;\n}): string | null {\n const parts = [\n changes > 0 ? plural(changes, 'change', 'changes') : null,\n messages > 0 ? plural(messages, 'message', 'messages') : null,\n recordings > 0 ? plural(recordings, 'recording', 'recordings') : null,\n ].filter((part): part is string => part !== null);\n return parts.length === 0 ? null : `Waiting to sync: ${parts.join(', ')}`;\n}\n\n/**\n * Where one item in the \"Waiting to sync\" list stands: a change, a message or a\n * recording made while Athena couldn't be reached.\n */\nexport type WaitingItemState =\n /** Saved on the device; it syncs when Athena is back. */\n | 'waiting'\n /** Being sent to Athena now. */\n | 'syncing'\n /**\n * Kept only while this tab or app stays open: it couldn't be saved on the\n * device (the organization keeps nothing on devices, or storage failed).\n */\n | 'in-memory'\n /**\n * Saved on the device in something that isn't open (a document closed before\n * its edits synced, one made offline): it syncs when it is next opened with a\n * connection (opening it offline sends nothing).\n */\n | 'when-opened'\n /** Made in another workspace: it syncs once that workspace is open again, with a connection. */\n | 'other-workspace'\n /** It doesn't go on its own: the person chooses (a message that waited more than a day). */\n | 'needs-you'\n /** Athena didn't take it: the app adds why, and what the person can do about it. */\n | 'not-synced';\n\n/** The state of one item in the \"Waiting to sync\" list, and the line that explains it. */\nexport function waitingItemCopy({\n state,\n device,\n}: {\n state: WaitingItemState;\n device: DeviceNoun;\n}): LabelCopy {\n switch (state) {\n case 'waiting':\n return {\n label: 'Waiting to sync',\n detail: `Saved on ${device}. It syncs when Athena is back.`,\n };\n case 'syncing':\n return { label: 'Syncing…', detail: `Sending what was saved on ${device}.` };\n case 'in-memory':\n return {\n label: 'Not saved yet',\n detail: `Kept only until you close ${openContainer(device)}. It syncs when Athena is back.`,\n };\n case 'when-opened':\n return {\n label: 'Not synced yet',\n detail: `Saved on ${device}. It syncs when you next open it with a connection.`,\n };\n case 'other-workspace':\n return {\n label: 'Waiting for its workspace',\n detail: 'It syncs once you’re in the workspace it was made in, with a connection.',\n };\n case 'needs-you':\n return { label: 'Waiting for you', detail: 'Open it to choose what happens to it.' };\n case 'not-synced':\n return { label: 'Couldn’t sync', detail: 'Athena didn’t take it.' };\n }\n}\n\n/** The headings of the \"Waiting to sync\" list, one per kind of item. */\nexport const WAITING_SECTION_TITLES = {\n changes: 'Changes',\n messages: 'Messages',\n recordings: 'Recordings',\n} as const;\n\n/**\n * What the person can do with an item Athena didn't take (`not-synced`) in the\n * \"Waiting to sync\" list: send it again, or delete the copy on the device. That\n * copy is the item's only one, so the app asks first (`deleteFromDeviceConfirm`)\n * and never deletes it as a side effect of anything else.\n */\nexport function notSyncedActions({ device }: { device: DeviceNoun }): {\n retry: string;\n remove: string;\n} {\n return { retry: 'Try again', remove: `Delete from ${device}` };\n}\n\n/** One of `notSyncedActions`, or showing where the item is kept on the device. */\nexport type NotSyncedAction = 'retry' | 'reveal' | 'remove';\n\n/**\n * What the person hears when the device couldn't do what they asked for an\n * item Athena didn't take (its files, not Athena, failed).\n */\nexport function notSyncedActionFailed({\n action,\n device,\n}: {\n action: NotSyncedAction;\n device: DeviceNoun;\n}): string {\n switch (action) {\n case 'retry':\n return 'Couldn’t send it again. Try again in a moment.';\n case 'reveal':\n return `Couldn’t show where it’s kept on ${device}.`;\n case 'remove':\n return `Couldn’t delete it from ${device}.`;\n }\n}\n\n/**\n * The question before deleting such an item's copy on the device, naming the\n * item: the destructive choice is never the default.\n */\nexport function deleteFromDeviceConfirm({\n name,\n device,\n}: {\n name: string;\n device: DeviceNoun;\n}): StatusCopy & { confirm: string; cancel: string } {\n return {\n title: `Delete “${name}” from ${device}?`,\n detail: 'Athena didn’t take it, so this is its only copy. Deleting it can’t be undone.',\n confirm: 'Delete',\n cancel: 'Cancel',\n };\n}\n"],"names":[],"mappings":";;AA6QO,MAAM,6BAA6B;AAsF1C,MAAM,kBAAkB;AAMjB,MAAM,mBAA8B;AAAA,EACzC,OAAO;AAAA,EACP,QAAQ,sBAAsB,eAAe;AAC/C;AAaO,SAAS,0BAA0B;AAAA,EACxC;AAAA,EACA;AACF,GAGW;AACT,QAAM,YAAY,aAA4C,QAAQ,kBAAkB,eAAe;AACvG,SAAO,eACH,iEAAiE,SAAS,KAC1E;AACN;AAGA,SAAS,WAAW,WAA4B;AAC9C,SAAO,YAAY,6BAA6B;AAClD;AAQO,SAAS,+BAA+B;AAAA,EAC7C,YAAY;AACd,IAEI,IAAY;AACd,SAAO,sDAAsD,WAAW,SAAS,CAAC,kBAChF,YAAY,OAAO,gBACrB;AACF;AAOO,SAAS,6BAA6B;AAAA,EAC3C,YAAY;AACd,IAEI,IAAY;AACd,SAAO,gFAAgF;AAAA,IACrF;AAAA,EAAA,CACD;AACH;;;;;;"}
|
|
1
|
+
{"version":3,"file":"offline-status.cjs","sources":["../../../../agent-runtime/src/offline-status.ts"],"sourcesContent":["/**\n * Athena's offline language: the one set of states and words every app uses to\n * say where the person stands when the network, or Athena, is out of reach —\n * Olympus, the desktop app, the phone app, the Chrome extension and the Office\n * add-in.\n *\n * The rule the words follow: say what still works before what doesn't. Work\n * done while Athena can't be reached is kept on the device and syncs when\n * Athena is back, so every message leads with that promise. Nothing here is\n * shown while Athena answers and nothing waits; the \"back\" messages are brief.\n *\n * Pure strings and small functions, no UI library: each app draws them its own\n * way (a banner, a header chip, a tooltip, a sheet). Reachability itself is\n * each app's own (the browser's or OS's online flag AND agora answering its\n * health read); this module only names the result.\n */\n\n/** Where the app stands with Athena. */\nexport type ConnectionState =\n /** Athena answers. Nothing is shown. */\n | 'online'\n /** The device has no network. */\n | 'offline'\n /** The network is up but Athena doesn't answer. */\n | 'unreachable'\n /** Athena answers again and what waited is being sent. */\n | 'reconnecting'\n /** Just back, and everything that waited was sent. Shown briefly. */\n | 'synced';\n\n/** Where one document's edits stand, for the chip in its header. */\nexport type DocumentSyncState =\n /** In Athena. */\n | 'saved'\n /** Being sent to Athena. */\n | 'saving'\n /** Kept on this device while Athena can't be reached; syncs when it is back. */\n | 'on-device'\n /** Kept only while this tab or app stays open (no device storage); syncs when Athena is back. */\n | 'in-memory'\n /** Sending what was kept on this device. */\n | 'syncing'\n /** Some changes overlap with Athena's version: the person chooses. */\n | 'needs-review'\n /** The document can no longer take these edits (access or state changed): kept, not sent. */\n | 'view-only'\n /**\n * Athena answers but isn't taking this document's changes for now: it holds the document while it\n * repairs it, or hasn't confirmed the latest changes for longer than usual (`SavingDelayCause`\n * says which). Kept, and saved once Athena takes them. Never said while Athena can't be reached:\n * that is `on-device` or `in-memory`.\n */\n | 'delayed';\n\n/** Why a document's saving is delayed while Athena answers (`DocumentSyncState` `delayed`). */\nexport type SavingDelayCause =\n /** Athena holds the document while it repairs it, and takes no changes to it until then. */\n | 'repairing'\n /** Athena hasn't confirmed the latest changes for longer than usual. */\n | 'unconfirmed';\n\n/** How a sentence names the device the person is on. */\nexport type DeviceNoun =\n | 'this browser'\n | 'this Mac'\n | 'this computer'\n | 'this phone'\n | 'this tablet'\n | 'this device';\n\n/**\n * What the app really keeps of work done while Athena can't be reached, so the\n * words never promise more: `device` (stored on the device, survives closing),\n * `memory` (kept only while this tab or app stays open: a tenant that turns\n * device copies off, or storage the app can't open), `none` (a surface that\n * keeps no work offline at all, like the Chrome side panel).\n */\nexport type OfflineKeeping = 'device' | 'memory' | 'none';\n\n/** A title and the one line under it (a banner, a sheet, a tooltip's heading). */\nexport interface StatusCopy {\n title: string;\n detail: string;\n}\n\n/** A short label for a chip or a disabled control, and its one-line explanation. */\nexport interface LabelCopy {\n label: string;\n detail: string;\n}\n\nfunction plural(count: number, one: string, many: string): string {\n return `${count} ${count === 1 ? one : many}`;\n}\n\n/** What a tab or an app is called when work lasts only while it stays open. */\nfunction openContainer(device: DeviceNoun): string {\n return device === 'this browser' ? 'this tab' : 'the app';\n}\n\n/** The device noun at the start of a sentence (\"This browser can…\"). */\nfunction deviceAtStart(device: DeviceNoun): string {\n return `${device.charAt(0).toUpperCase()}${device.slice(1)}`;\n}\n\n/**\n * The sentence a promise ends with when the device can remove what it keeps before it syncs\n * (`evictable`: a browser that hasn't granted persistent storage, a private window): \"before then\"\n * after a sentence that says when it syncs, \"before it syncs\" after one that doesn't.\n */\nfunction removableBeforeSync({\n device,\n count,\n before = 'then',\n}: {\n device: DeviceNoun;\n count: number;\n before?: 'then' | 'syncing';\n}): string {\n const what = count === 1 ? 'it' : 'them';\n const when = before === 'then' ? 'then' : count === 1 ? 'it syncs' : 'they sync';\n return `${deviceAtStart(device)} can remove ${what} before ${when}.`;\n}\n\n/** The promise after \"You're offline\" / \"Can't reach Athena\", honest to what is kept. */\nfunction keepingPromise({\n keeping,\n device,\n when,\n evictable,\n}: {\n keeping: OfflineKeeping;\n device: DeviceNoun;\n when: string;\n evictable: boolean;\n}): string {\n switch (keeping) {\n case 'device':\n return evictable\n ? `You can keep working. Changes are kept on ${device} and sync ${when}. ${removableBeforeSync({ device, count: 2 })}`\n : `You can keep working. Changes are saved on ${device} and sync ${when}.`;\n case 'memory':\n return `You can keep working, but changes are kept only until you close ${openContainer(device)}. They sync ${when}.`;\n case 'none':\n return `You’re still signed in. This reconnects ${when}.`;\n }\n}\n\n/**\n * The app-wide status line, or `null` while Athena answers and nothing waits.\n * `waiting` counts changes not in Athena yet: while `reconnecting` they are\n * being sent; while `online` they wait for something else (a copy that sends\n * when it is next opened), and the line stays until they are gone.\n * `keeping` is what the app really keeps offline (default `device`). A surface\n * that keeps nothing (`none`) has nothing to sync, so its way back is only\n * \"Back online\", never \"Syncing…\" or \"All changes synced\".\n * `evictable`: the device may remove what it keeps before it syncs (a browser that hasn't granted\n * persistent storage, a private window); only `keeping` `device` says so (default `false`).\n */\nexport function connectionCopy({\n state,\n device,\n waiting = 0,\n keeping = 'device',\n evictable = false,\n}: {\n state: ConnectionState;\n device: DeviceNoun;\n waiting?: number;\n keeping?: OfflineKeeping;\n evictable?: boolean;\n}): StatusCopy | null {\n const atRisk = evictable && keeping === 'device';\n switch (state) {\n case 'online':\n return waiting > 0\n ? {\n title: 'Not synced yet',\n detail: `${plural(waiting, 'change', 'changes')} on ${device} ${waiting === 1 ? 'hasn’t' : 'haven’t'} synced yet.${\n atRisk ? ` ${removableBeforeSync({ device, count: waiting, before: 'syncing' })}` : ''\n }`,\n }\n : null;\n case 'offline':\n return {\n title: 'You’re offline',\n detail: keepingPromise({\n keeping,\n device,\n when: 'when you’re back online',\n evictable: atRisk,\n }),\n };\n case 'unreachable':\n return {\n title: 'Can’t reach Athena',\n detail: keepingPromise({ keeping, device, when: 'when Athena is back', evictable: atRisk }),\n };\n case 'reconnecting':\n if (keeping === 'none') return { title: 'Back online', detail: '' };\n return {\n title: 'Back online',\n detail:\n waiting > 0 ? `Syncing ${plural(waiting, 'change', 'changes')}…` : 'Syncing…',\n };\n case 'synced':\n return keeping === 'none'\n ? { title: 'Back online', detail: '' }\n : { title: 'All changes synced', detail: '' };\n }\n}\n\n/**\n * The line under \"Saving delayed\": why saving waits, and what is kept meanwhile, never promising\n * more than the app keeps.\n */\nfunction savingDelayedDetail({\n delay,\n keeping,\n device,\n}: {\n delay: SavingDelayCause;\n keeping: Exclude<OfflineKeeping, 'none'>;\n device: DeviceNoun;\n}): string {\n if (delay === 'repairing') {\n return keeping === 'memory'\n ? `You can keep working, but your changes are kept only until you close ${openContainer(device)}. They save once Athena finishes repairing this document.`\n : `You can keep working. Your changes are kept on ${device} and save once Athena finishes repairing this document.`;\n }\n return keeping === 'memory'\n ? `Athena is taking longer than usual to save your latest changes. They’re kept only until you close ${openContainer(device)}, so keep it open.`\n : `Athena is taking longer than usual to save your latest changes. They’re kept on ${device} in the meantime.`;\n}\n\n/**\n * What `on-device` says. The desktop app on a Mac speaks as Word, Excel and PowerPoint for Mac do\n * of a cloud document whose changes wait on the Mac (Office's own title-bar words): \"Upload\n * Pending\", and \"Upload Pending (Offline)\" while Athena can't be reached. Every other app says the\n * changes are saved on its device: a browser has no such words (Word for the web can't edit\n * offline), nor does the phone.\n */\nfunction onDeviceCopy({ device, reachable }: { device: DeviceNoun; reachable: boolean }): LabelCopy {\n if (device === 'this Mac') {\n return {\n label: reachable ? 'Upload Pending' : 'Upload Pending (Offline)',\n detail: 'Upload pending. Your changes sync when Athena is back.',\n };\n }\n return {\n label: `Saved on ${device}`,\n detail: `Saved on ${device}. Your changes sync when Athena is back.`,\n };\n}\n\n/**\n * The chip in a document's header, and the line it explains itself with. For `delayed`, `delay`\n * says why (default `unconfirmed`) and `keeping` whether the changes are kept on the device or only\n * while the tab or app stays open (default `device`); every other state ignores both. `reachable`\n * (default `true`) is whether Athena can be reached, which only the Mac's `on-device` words read.\n * For `on-device`, `evictable` says the device may remove the changes before they sync (default\n * `false`).\n */\nexport function documentSyncCopy({\n state,\n device,\n delay = 'unconfirmed',\n keeping = 'device',\n reachable = true,\n evictable = false,\n}: {\n state: DocumentSyncState;\n device: DeviceNoun;\n delay?: SavingDelayCause;\n keeping?: Exclude<OfflineKeeping, 'none'>;\n reachable?: boolean;\n evictable?: boolean;\n}): LabelCopy {\n switch (state) {\n case 'saved':\n return { label: 'Saved', detail: 'Saved to Athena.' };\n case 'saving':\n return { label: 'Saving…', detail: 'Saving to Athena.' };\n case 'on-device': {\n const copy = onDeviceCopy({ device, reachable });\n return evictable\n ? { ...copy, detail: `${copy.detail} ${removableBeforeSync({ device, count: 2 })}` }\n : copy;\n }\n case 'in-memory':\n return {\n label: 'Not saved yet',\n detail: `Your changes are kept only until you close ${openContainer(device)}, and save when Athena is back.`,\n };\n case 'syncing':\n return { label: 'Syncing…', detail: `Sending the changes saved on ${device}.` };\n case 'needs-review':\n return {\n label: 'Review changes',\n detail: 'Some of your changes overlap with changes made in Athena. Choose what to keep.',\n };\n case 'view-only':\n return {\n label: 'View only',\n detail: `You can’t edit this anymore. Your changes stay on ${device}.`,\n };\n case 'delayed':\n return { label: 'Saving delayed', detail: savingDelayedDetail({ delay, keeping, device }) };\n }\n}\n\n/**\n * How long a document's edits may wait unconfirmed on a connection that is up before its saving\n * reads as delayed (`delayed`, `unconfirmed`), and before its room asks Keryx why: the stall window\n * every editor shares (`ACK_STALL_MS` in `@athenaintel/asset-host/local-updates`, which Olympus's\n * `room-sync-store.test.ts` holds equal).\n */\nexport const UNCONFIRMED_EDITS_STALL_MS = 15_000;\n\n/**\n * Whether a document's saving is delayed, and why (`DocumentSyncState` `delayed`): only while Athena\n * answers and the document's room is connected; `repairing` when Keryx refuses the room's writes\n * (quarantined while it is repaired: said at once, before anything waits), `unconfirmed` once its\n * edits have stayed unconfirmed past {@link UNCONFIRMED_EDITS_STALL_MS}; `null` otherwise. Every\n * app's chip asks it after a hold and after edits kept without a connection, and ahead of syncing\n * and saving, so a room that refuses writes never spins and being offline is never a delay.\n */\nexport function savingDelayCause({\n reachable,\n connected,\n writesRefused,\n unconfirmedStalled,\n}: {\n /** Athena answers (the app's reachability, never the socket alone). */\n reachable: boolean;\n /** The document's room is connected. */\n connected: boolean;\n /** Keryx's document status says the room takes no writes for now. */\n writesRefused: boolean;\n /** Local edits have waited unconfirmed past the stall window on this connection. */\n unconfirmedStalled: boolean;\n}): SavingDelayCause | null {\n if (!reachable || !connected) return null;\n if (writesRefused) return 'repairing';\n return unconfirmedStalled ? 'unconfirmed' : null;\n}\n\n/**\n * What the person lost of a document while it was open, when the server checked its room's grant\n * again (Keryx's 4401, after a share changed): `edit`, they may still view it but no longer edit it;\n * `view`, they may no longer open it at all.\n */\nexport type AccessLoss = 'edit' | 'view';\n\n/** What a document says once the person's access changed while it was open. */\nexport interface AccessChangedCopy extends StatusCopy {\n /**\n * The label of the button that saves the person's version as a file of their own, with the\n * changes Athena never took, or `null` when nothing is offered: nothing of theirs waits, or the\n * device no longer serves the document (`view`).\n */\n saveCopy: string | null;\n}\n\n/**\n * What a document says once the person's access to it changed while it was open (its editor turns\n * read-only at once), in Word's own words (Word for Mac's message bar, `strman_a6d4`, and its\n * \"Permissions Have Changed\" notice, `strman_0a3e`). `keptChanges`: changes Athena never took wait\n * on the device, never sent; Word offers to save a copy of the file so nothing typed is lost. A\n * `view` loss offers nothing to save: the device stops serving a document the person may no longer\n * read, and their changes stay where they are kept.\n */\nexport function accessChangedCopy({\n loss,\n keptChanges,\n}: {\n loss: AccessLoss;\n keptChanges: boolean;\n}): AccessChangedCopy {\n if (loss === 'view') {\n return {\n title: 'Permissions Have Changed',\n detail:\n 'The permissions on this file were updated and you no longer have rights to view this file. If you previously made changes to this file, they may not be saved.',\n saveCopy: null,\n };\n }\n if (keptChanges) {\n return {\n title: 'Can’t save',\n detail:\n 'You no longer have the permission to edit this file. To keep your changes, save a copy of the file.',\n saveCopy: 'Save a Copy',\n };\n }\n return {\n title: 'Read Only',\n detail: 'You no longer have the permission to edit this file.',\n saveCopy: null,\n };\n}\n\n/** What a document says while it holds changes an older version of Athena made on the device. */\nexport interface OlderVersionChangesCopy extends StatusCopy {\n /** Saves this device's version as a file of the person's own, those changes included (Word's). */\n saveCopy: string;\n /** Sets this device's version aside, kept on the device to compare, and opens Athena's. */\n useAthenaVersion: string;\n /** Athena's version couldn't be opened: another window of the device has the document open. */\n openElsewhere: string;\n}\n\n/**\n * What a document says while its local copy holds changes an older version of Athena made on this\n * device, below its room's write floor (`older-version`, `@athenaintel/local-copy/room-copy`): such\n * a version deletes what newer ones store, so those changes are never uploaded. Word's way out,\n * as when it can't save a cloud document (\"You can save a copy to prevent losing your work\",\n * `strman`): Save a Copy, and Athena's version, which keeps this device's version on it to compare\n * and copy from until the person discards it. `saveCopy: false` for an editor that offers no Save\n * a Copy of the live document (the phone's page): its kept version downloads instead.\n */\nexport function olderVersionChangesCopy({\n device,\n noun,\n saveCopy = true,\n}: {\n device: DeviceNoun;\n /** What the person calls the document (\"document\", \"spreadsheet\"). */\n noun: string;\n /** Whether the editor offers Save a Copy beside Athena's version. */\n saveCopy?: boolean;\n}): OlderVersionChangesCopy {\n const made = `You changed this ${noun} on ${device} in an older version of Athena, so those changes can’t be uploaded.`;\n return {\n title: 'Changes made in an older version of Athena',\n detail: saveCopy\n ? `${made} To keep them, save a copy, or use Athena’s version: yours stays on ${device} to compare and copy from.`\n : `${made} Use Athena’s version to keep yours on ${device}, where you can compare it, copy from it or download it.`,\n saveCopy: 'Save a Copy',\n useAthenaVersion: 'Use Athena’s version',\n openElsewhere: `This ${noun} is open in another window. Close it there, then try again.`,\n };\n}\n\n/** When something that needs Athena works again (the second half of `NEEDS_CONNECTION`'s line). */\nconst AVAILABLE_AGAIN = 'It’s available again when you’re back online.';\n\n/**\n * The reason on a control that needs Athena (share, run, upload, invite…),\n * shown disabled while Athena can't be reached rather than failing when used.\n */\nexport const NEEDS_CONNECTION: LabelCopy = {\n label: 'Needs a connection',\n detail: `This needs Athena. ${AVAILABLE_AGAIN}`,\n};\n\n/** What a composer was given to attach: a photo (from the phone's library or camera) or any file. */\nexport type AttachmentKind = 'photo' | 'file';\n\n/**\n * What a composer says when a photo or file is picked, pasted or dropped while\n * Athena can't be reached. It isn't attached: only Athena can take the upload.\n * In a chat whose messages wait to be sent meanwhile (`messagesWait`: the chat\n * keeps an outbox), the message itself still goes, so that comes first. Where\n * nothing waits (the Chrome side panel, a launcher that keeps its draft), it\n * says only when attaching works again, as `NEEDS_CONNECTION` does.\n */\nexport function attachmentNeedsConnection({\n kind,\n messagesWait,\n}: {\n kind: AttachmentKind;\n messagesWait: boolean;\n}): string {\n const attaching = `Attaching ${kind === 'photo' ? 'a photo' : 'a file'} needs Athena. ${AVAILABLE_AGAIN}`;\n return messagesWait\n ? `You can still send your message: it goes when Athena is back. ${attaching}`\n : attaching;\n}\n\n/** What a sent message carries besides its text that can't wait for Athena. */\nfunction heldExtras(withQuote: boolean): string {\n return withQuote ? 'an attachment or a quote' : 'an attachment';\n}\n\n/**\n * What a composer says when a message is sent while Athena can't be reached\n * with an attachment that never reached Athena (or, `withQuote`, a quote the\n * chat can't keep waiting). Its text could wait in the chat outbox; the\n * attachment can't, so the draft stays as it is.\n */\nexport function attachedMessageNeedsConnection({\n withQuote = false,\n}: {\n withQuote?: boolean;\n} = {}): string {\n return `Your text can wait and go when Athena is back, but ${heldExtras(withQuote)} can’t. Remove ${\n withQuote ? 'it' : 'the attachment'\n } to send the text.`;\n}\n\n/**\n * What a composer says when a message with an attachment (or, `withQuote`, a\n * quote) is sent while messages written when Athena couldn't be reached still\n * wait to go: those go first, and this one can't wait behind them.\n */\nexport function attachedMessageBehindWaiting({\n withQuote = false,\n}: {\n withQuote?: boolean;\n} = {}): string {\n return `Messages you wrote while Athena couldn’t be reached go first. A message with ${heldExtras(\n withQuote\n )} can’t wait behind them, so send it once they’ve gone.`;\n}\n\n/**\n * The reason on New Presentation while Athena can't be reached, in a workspace\n * whose new presentations start from its design system's template: a\n * presentation made offline starts blank, and only Athena can copy the\n * template, so none is made until it can.\n */\nexport const NEW_PRESENTATION_NEEDS_TEMPLATE: LabelCopy = {\n label: 'Needs a connection',\n detail:\n 'New presentations in this workspace start from its design template, which needs Athena. You can make one when you’re back online.',\n};\n\n/**\n * What a download of a spreadsheet made from an Excel Macro-Enabled Workbook\n * (`.xlsm`) says when it went without the macros because they come from Athena,\n * which can't be reached, and the device hasn't kept them (they travel with the\n * workbook and never run). The file is the workbook without them; downloading\n * it again with a connection keeps them.\n */\nexport function downloadedWithoutMacros({ device }: { device: DeviceNoun }): string {\n return `Downloaded without the spreadsheet’s macros: they come from Athena, which can’t be reached, and ${device} hasn’t kept them. Download it again when you’re back online to keep them.`;\n}\n\n/**\n * Save's question for a spreadsheet whose file is an Excel Macro-Enabled\n * Workbook (`.xlsm`) while its macros come from Athena, which can't be reached,\n * and the device hasn't kept them: the `.xlsm` is never written without them,\n * so the person may save an Excel Workbook (`.xlsx`) beside it, and Save writes\n * the `.xlsm` again once Athena answers.\n */\nexport function macrosNeedConnectionToSave({\n name,\n device,\n}: {\n name: string;\n device: DeviceNoun;\n}): StatusCopy {\n return {\n title: `Athena can’t save the macros in “${name}” right now.`,\n detail: `They come from Athena, which can’t be reached, and ${device} hasn’t kept them. Save it as an Excel Workbook (.xlsx) instead. “${name}” stays as it is, macros included, and Save writes it again when you’re back online.`,\n };\n}\n\n/**\n * What saving a version kept on the device as a new spreadsheet says when the\n * spreadsheet was made from an Excel Macro-Enabled Workbook (`.xlsm`) and\n * Athena can't be reached: the new spreadsheet is made all the same (on the\n * device, and it syncs when Athena is back), but without the macros, which only\n * Athena can keep with a new spreadsheet (they travel with the workbook and\n * never run). The kept version stays on the device, so saving it again with a\n * connection keeps them.\n */\nexport function savedAsNewWithoutMacros({ device }: { device: DeviceNoun }): string {\n return `The new spreadsheet was made without the macros: only Athena can keep them in a new spreadsheet, and it can’t be reached. The kept version stays on ${device}: save it as a new spreadsheet again when you’re back online to keep them.`;\n}\n\n/**\n * What saving a copy of a Word document says when the copy was made on the\n * device while Athena can't be reached, for a document whose original Word file\n * Athena keeps (one imported from Word): the copy is made all the same, from the\n * document as the device holds it, over the original file kept with it, so it\n * keeps that file's parts but writes its body from what the editor shows.\n * Athena's own copy, made from the original file, keeps everything, so saving\n * a copy again with a connection makes that one.\n */\nexport function wordCopyMadeOnDevice({ device }: { device: DeviceNoun }): StatusCopy {\n return {\n title: `Copy made on ${device}`,\n detail: `Athena can’t be reached, so the copy holds the document as it is on ${device}. Anything in the original Word file that Athena doesn’t show may be missing from it: save a copy again when you’re back online for the complete file.`,\n };\n}\n\n/**\n * Whether an item opens without a connection on this device, for lists.\n * `canKeep` says whether opening it online would keep a copy here (the\n * deployment allows device copies and the item's type is one that is kept):\n * only then is the person told to open it once.\n */\nexport function offlineAvailabilityCopy({\n available,\n canKeep,\n device,\n}: {\n available: boolean;\n canKeep: boolean;\n device: DeviceNoun;\n}): LabelCopy {\n if (available) {\n return { label: 'Available offline', detail: `Opens without a connection on ${device}.` };\n }\n return canKeep\n ? {\n label: 'Not available offline',\n detail: `Open it once while online to keep it on ${device}.`,\n }\n : { label: 'Not available offline', detail: 'This opens only with a connection.' };\n}\n\n/**\n * The one-time explainer shown the first time the person goes offline, on a\n * surface that keeps work there (`keeping` `device`). A surface that keeps\n * nothing (`none`: the Chrome side panel, the Office task pane) shows no\n * explainer; its status line already says all that holds.\n */\nexport function firstOfflineExplainer({ device }: { device: DeviceNoun }): StatusCopy & {\n action: string;\n} {\n return {\n title: 'You can keep working offline',\n detail: `Documents you’ve opened recently keep working on ${device}. Your changes sync when you’re back online.`,\n action: 'Got it',\n };\n}\n\n/**\n * How a document's changes could be lost before they reach Athena:\n *\n * - `device`: saved on the device until they reach Athena (no connection, or a review the person\n * has not finished), but the device can remove them before then (a browser that hasn't granted\n * persistent storage, a private window);\n * - `held`: saved on the device and can't be sent (the person can only view the document now, or\n * Athena refused it), and the device can remove them;\n * - `memory`: kept only until the tab or app closes (the device keeps no copy).\n */\nexport type ChangesAtRisk = 'device' | 'held' | 'memory';\n\n/**\n * What an editor says above a document whose changes could be lost before they reach Athena, with\n * the copy it offers of them (Word's \"Upload pending\" and \"Changes not saved\"), by\n * {@link ChangesAtRisk}.\n */\nexport function changesAtRiskCopy({\n risk,\n device,\n}: {\n risk: ChangesAtRisk;\n device: DeviceNoun;\n}): StatusCopy & { download: string; downloadFailed: string } {\n const actions = {\n download: 'Download a copy',\n downloadFailed: 'Couldn’t download a copy. Try again.',\n };\n switch (risk) {\n case 'memory':\n return {\n title: 'Changes not saved',\n detail: `Your changes are kept only until you close ${openContainer(device)}. Download a copy to keep them.`,\n ...actions,\n };\n case 'held':\n return {\n title: 'Changes not saved',\n detail: `Your changes can’t be sent, and they’re saved only on ${device}, which can remove them. Download a copy to keep them.`,\n ...actions,\n };\n case 'device':\n return {\n title: 'Upload pending',\n detail: `Your changes are saved only on ${device} until they reach Athena, and ${device} can remove them before then. Download a copy to keep them.`,\n ...actions,\n };\n }\n}\n\n/**\n * The action that asks the device to keep what it holds until it syncs, where asking shows the\n * person a question of the device's own (Firefox asks whether the site may store data in\n * persistent storage): the app says why first (`changesAtRiskCopy`) and asks only on this press.\n */\nexport function keepOnDeviceAction({ device }: { device: DeviceNoun }): string {\n return `Keep them on ${device}`;\n}\n\n/** A browser whose own way of keeping a site's data longer the words point to. */\nexport type SaferStorageBrowser = 'safari-mac' | 'safari-ios';\n\n/**\n * Where changes made offline in Safari are kept longer: Safari keeps a site added to the Dock or\n * the Home Screen as an app of its own, which it is likelier to let keep its data, and the desktop\n * app keeps them in storage of its own.\n */\nexport function saferStorageHint({ browser }: { browser: SaferStorageBrowser }): string {\n return browser === 'safari-mac'\n ? 'To keep changes made offline safer in Safari, add Athena to the Dock (File ▸ Add to Dock), or use Athena Desktop.'\n : 'To keep changes made offline safer, add Athena to your Home Screen (Share ▸ Add to Home Screen).';\n}\n\n/**\n * What the app says when it finds that changes it kept on the device never reached Athena and the\n * device no longer has them (the browser removed them before they synced), naming how many\n * documents; each document's row says when its changes were made (`lostChangesItemCopy`).\n */\nexport function lostChangesCopy({\n device,\n documents,\n}: {\n device: DeviceNoun;\n documents: number;\n}): StatusCopy & { why: string; open: string; dismiss: string } {\n const which = documents === 1 ? 'a document' : plural(documents, 'document', 'documents');\n return {\n title: 'Changes not saved',\n detail: `Changes you made to ${which} on ${device} never reached Athena, and ${device} no longer has them.`,\n why: 'Browsers can remove what a site keeps when they run short of space, or when the site hasn’t been opened for a while.',\n open: 'Open',\n dismiss: 'Dismiss',\n };\n}\n\n/** One document's row under `lostChangesCopy`: the words before when its changes were made. */\nexport function lostChangesItemCopy({ device }: { device: DeviceNoun }): string {\n return `Changed on ${device}`;\n}\n\n/**\n * The summary heading of the one \"Waiting to sync\" list, from what waits on\n * the device; `null` when nothing does.\n */\nexport function waitingSummary({\n changes = 0,\n messages = 0,\n recordings = 0,\n}: {\n changes?: number;\n messages?: number;\n recordings?: number;\n}): string | null {\n const parts = [\n changes > 0 ? plural(changes, 'change', 'changes') : null,\n messages > 0 ? plural(messages, 'message', 'messages') : null,\n recordings > 0 ? plural(recordings, 'recording', 'recordings') : null,\n ].filter((part): part is string => part !== null);\n return parts.length === 0 ? null : `Waiting to sync: ${parts.join(', ')}`;\n}\n\n/**\n * Where one item in the \"Waiting to sync\" list stands: a change, a message or a\n * recording made while Athena couldn't be reached.\n */\nexport type WaitingItemState =\n /** Saved on the device; it syncs when Athena is back. */\n | 'waiting'\n /** Being sent to Athena now. */\n | 'syncing'\n /**\n * Kept only while this tab or app stays open: it couldn't be saved on the\n * device (the organization keeps nothing on devices, or storage failed).\n */\n | 'in-memory'\n /**\n * Saved on the device in something that isn't open (a document closed before\n * its edits synced, one made offline): it syncs when it is next opened with a\n * connection (opening it offline sends nothing).\n */\n | 'when-opened'\n /** Made in another workspace: it syncs once that workspace is open again, with a connection. */\n | 'other-workspace'\n /** It doesn't go on its own: the person chooses (a message that waited more than a day). */\n | 'needs-you'\n /** Athena didn't take it: the app adds why, and what the person can do about it. */\n | 'not-synced';\n\n/**\n * The state of one item in the \"Waiting to sync\" list, and the line that explains it. For an item\n * saved on the device (`waiting`, `when-opened`), `evictable` says the device may remove it before\n * it syncs (default `false`).\n */\nexport function waitingItemCopy({\n state,\n device,\n evictable = false,\n}: {\n state: WaitingItemState;\n device: DeviceNoun;\n evictable?: boolean;\n}): LabelCopy {\n const removable = evictable ? ` ${removableBeforeSync({ device, count: 1 })}` : '';\n switch (state) {\n case 'waiting':\n return {\n label: 'Waiting to sync',\n detail: `Saved on ${device}. It syncs when Athena is back.${removable}`,\n };\n case 'syncing':\n return { label: 'Syncing…', detail: `Sending what was saved on ${device}.` };\n case 'in-memory':\n return {\n label: 'Not saved yet',\n detail: `Kept only until you close ${openContainer(device)}. It syncs when Athena is back.`,\n };\n case 'when-opened':\n return {\n label: 'Not synced yet',\n detail: `Saved on ${device}. It syncs when you next open it with a connection.${removable}`,\n };\n case 'other-workspace':\n return {\n label: 'Waiting for its workspace',\n detail: 'It syncs once you’re in the workspace it was made in, with a connection.',\n };\n case 'needs-you':\n return { label: 'Waiting for you', detail: 'Open it to choose what happens to it.' };\n case 'not-synced':\n return { label: 'Couldn’t sync', detail: 'Athena didn’t take it.' };\n }\n}\n\n/** The headings of the \"Waiting to sync\" list, one per kind of item. */\nexport const WAITING_SECTION_TITLES = {\n changes: 'Changes',\n messages: 'Messages',\n recordings: 'Recordings',\n} as const;\n\n/**\n * What the person can do with an item Athena didn't take (`not-synced`) in the\n * \"Waiting to sync\" list: send it again, or delete the copy on the device. That\n * copy is the item's only one, so the app asks first (`deleteFromDeviceConfirm`)\n * and never deletes it as a side effect of anything else.\n */\nexport function notSyncedActions({ device }: { device: DeviceNoun }): {\n retry: string;\n remove: string;\n} {\n return { retry: 'Try again', remove: `Delete from ${device}` };\n}\n\n/** One of `notSyncedActions`, or showing where the item is kept on the device. */\nexport type NotSyncedAction = 'retry' | 'reveal' | 'remove';\n\n/**\n * What the person hears when the device couldn't do what they asked for an\n * item Athena didn't take (its files, not Athena, failed).\n */\nexport function notSyncedActionFailed({\n action,\n device,\n}: {\n action: NotSyncedAction;\n device: DeviceNoun;\n}): string {\n switch (action) {\n case 'retry':\n return 'Couldn’t send it again. Try again in a moment.';\n case 'reveal':\n return `Couldn’t show where it’s kept on ${device}.`;\n case 'remove':\n return `Couldn’t delete it from ${device}.`;\n }\n}\n\n/**\n * The question before deleting such an item's copy on the device, naming the\n * item: the destructive choice is never the default.\n */\nexport function deleteFromDeviceConfirm({\n name,\n device,\n}: {\n name: string;\n device: DeviceNoun;\n}): StatusCopy & { confirm: string; cancel: string } {\n return {\n title: `Delete “${name}” from ${device}?`,\n detail: 'Athena didn’t take it, so this is its only copy. Deleting it can’t be undone.',\n confirm: 'Delete',\n cancel: 'Cancel',\n };\n}\n"],"names":[],"mappings":";;AA6TO,MAAM,6BAA6B;AAgI1C,MAAM,kBAAkB;AAMjB,MAAM,mBAA8B;AAAA,EACzC,OAAO;AAAA,EACP,QAAQ,sBAAsB,eAAe;AAC/C;AAaO,SAAS,0BAA0B;AAAA,EACxC;AAAA,EACA;AACF,GAGW;AACT,QAAM,YAAY,aAA4C,QAAQ,kBAAkB,eAAe;AACvG,SAAO,eACH,iEAAiE,SAAS,KAC1E;AACN;AAGA,SAAS,WAAW,WAA4B;AAC9C,SAAO,YAAY,6BAA6B;AAClD;AAQO,SAAS,+BAA+B;AAAA,EAC7C,YAAY;AACd,IAEI,IAAY;AACd,SAAO,sDAAsD,WAAW,SAAS,CAAC,kBAChF,YAAY,OAAO,gBACrB;AACF;AAOO,SAAS,6BAA6B;AAAA,EAC3C,YAAY;AACd,IAEI,IAAY;AACd,SAAO,gFAAgF;AAAA,IACrF;AAAA,EAAA,CACD;AACH;;;;;;"}
|
|
@@ -83,25 +83,31 @@ export interface LabelCopy {
|
|
|
83
83
|
* `keeping` is what the app really keeps offline (default `device`). A surface
|
|
84
84
|
* that keeps nothing (`none`) has nothing to sync, so its way back is only
|
|
85
85
|
* "Back online", never "Syncing…" or "All changes synced".
|
|
86
|
+
* `evictable`: the device may remove what it keeps before it syncs (a browser that hasn't granted
|
|
87
|
+
* persistent storage, a private window); only `keeping` `device` says so (default `false`).
|
|
86
88
|
*/
|
|
87
|
-
export declare function connectionCopy({ state, device, waiting, keeping, }: {
|
|
89
|
+
export declare function connectionCopy({ state, device, waiting, keeping, evictable, }: {
|
|
88
90
|
state: ConnectionState;
|
|
89
91
|
device: DeviceNoun;
|
|
90
92
|
waiting?: number;
|
|
91
93
|
keeping?: OfflineKeeping;
|
|
94
|
+
evictable?: boolean;
|
|
92
95
|
}): StatusCopy | null;
|
|
93
96
|
/**
|
|
94
97
|
* The chip in a document's header, and the line it explains itself with. For `delayed`, `delay`
|
|
95
98
|
* says why (default `unconfirmed`) and `keeping` whether the changes are kept on the device or only
|
|
96
99
|
* while the tab or app stays open (default `device`); every other state ignores both. `reachable`
|
|
97
100
|
* (default `true`) is whether Athena can be reached, which only the Mac's `on-device` words read.
|
|
101
|
+
* For `on-device`, `evictable` says the device may remove the changes before they sync (default
|
|
102
|
+
* `false`).
|
|
98
103
|
*/
|
|
99
|
-
export declare function documentSyncCopy({ state, device, delay, keeping, reachable, }: {
|
|
104
|
+
export declare function documentSyncCopy({ state, device, delay, keeping, reachable, evictable, }: {
|
|
100
105
|
state: DocumentSyncState;
|
|
101
106
|
device: DeviceNoun;
|
|
102
107
|
delay?: SavingDelayCause;
|
|
103
108
|
keeping?: Exclude<OfflineKeeping, 'none'>;
|
|
104
109
|
reachable?: boolean;
|
|
110
|
+
evictable?: boolean;
|
|
105
111
|
}): LabelCopy;
|
|
106
112
|
/**
|
|
107
113
|
* How long a document's edits may wait unconfirmed on a connection that is up before its saving
|
|
@@ -155,6 +161,31 @@ export declare function accessChangedCopy({ loss, keptChanges, }: {
|
|
|
155
161
|
loss: AccessLoss;
|
|
156
162
|
keptChanges: boolean;
|
|
157
163
|
}): AccessChangedCopy;
|
|
164
|
+
/** What a document says while it holds changes an older version of Athena made on the device. */
|
|
165
|
+
export interface OlderVersionChangesCopy extends StatusCopy {
|
|
166
|
+
/** Saves this device's version as a file of the person's own, those changes included (Word's). */
|
|
167
|
+
saveCopy: string;
|
|
168
|
+
/** Sets this device's version aside, kept on the device to compare, and opens Athena's. */
|
|
169
|
+
useAthenaVersion: string;
|
|
170
|
+
/** Athena's version couldn't be opened: another window of the device has the document open. */
|
|
171
|
+
openElsewhere: string;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* What a document says while its local copy holds changes an older version of Athena made on this
|
|
175
|
+
* device, below its room's write floor (`older-version`, `@athenaintel/local-copy/room-copy`): such
|
|
176
|
+
* a version deletes what newer ones store, so those changes are never uploaded. Word's way out,
|
|
177
|
+
* as when it can't save a cloud document ("You can save a copy to prevent losing your work",
|
|
178
|
+
* `strman`): Save a Copy, and Athena's version, which keeps this device's version on it to compare
|
|
179
|
+
* and copy from until the person discards it. `saveCopy: false` for an editor that offers no Save
|
|
180
|
+
* a Copy of the live document (the phone's page): its kept version downloads instead.
|
|
181
|
+
*/
|
|
182
|
+
export declare function olderVersionChangesCopy({ device, noun, saveCopy, }: {
|
|
183
|
+
device: DeviceNoun;
|
|
184
|
+
/** What the person calls the document ("document", "spreadsheet"). */
|
|
185
|
+
noun: string;
|
|
186
|
+
/** Whether the editor offers Save a Copy beside Athena's version. */
|
|
187
|
+
saveCopy?: boolean;
|
|
188
|
+
}): OlderVersionChangesCopy;
|
|
158
189
|
/**
|
|
159
190
|
* The reason on a control that needs Athena (share, run, upload, invite…),
|
|
160
191
|
* shown disabled while Athena can't be reached rather than failing when used.
|
|
@@ -231,6 +262,18 @@ export declare function macrosNeedConnectionToSave({ name, device, }: {
|
|
|
231
262
|
export declare function savedAsNewWithoutMacros({ device }: {
|
|
232
263
|
device: DeviceNoun;
|
|
233
264
|
}): string;
|
|
265
|
+
/**
|
|
266
|
+
* What saving a copy of a Word document says when the copy was made on the
|
|
267
|
+
* device while Athena can't be reached, for a document whose original Word file
|
|
268
|
+
* Athena keeps (one imported from Word): the copy is made all the same, from the
|
|
269
|
+
* document as the device holds it, over the original file kept with it, so it
|
|
270
|
+
* keeps that file's parts but writes its body from what the editor shows.
|
|
271
|
+
* Athena's own copy, made from the original file, keeps everything, so saving
|
|
272
|
+
* a copy again with a connection makes that one.
|
|
273
|
+
*/
|
|
274
|
+
export declare function wordCopyMadeOnDevice({ device }: {
|
|
275
|
+
device: DeviceNoun;
|
|
276
|
+
}): StatusCopy;
|
|
234
277
|
/**
|
|
235
278
|
* Whether an item opens without a connection on this device, for lists.
|
|
236
279
|
* `canKeep` says whether opening it online would keep a copy here (the
|
|
@@ -253,6 +296,64 @@ export declare function firstOfflineExplainer({ device }: {
|
|
|
253
296
|
}): StatusCopy & {
|
|
254
297
|
action: string;
|
|
255
298
|
};
|
|
299
|
+
/**
|
|
300
|
+
* How a document's changes could be lost before they reach Athena:
|
|
301
|
+
*
|
|
302
|
+
* - `device`: saved on the device until they reach Athena (no connection, or a review the person
|
|
303
|
+
* has not finished), but the device can remove them before then (a browser that hasn't granted
|
|
304
|
+
* persistent storage, a private window);
|
|
305
|
+
* - `held`: saved on the device and can't be sent (the person can only view the document now, or
|
|
306
|
+
* Athena refused it), and the device can remove them;
|
|
307
|
+
* - `memory`: kept only until the tab or app closes (the device keeps no copy).
|
|
308
|
+
*/
|
|
309
|
+
export type ChangesAtRisk = 'device' | 'held' | 'memory';
|
|
310
|
+
/**
|
|
311
|
+
* What an editor says above a document whose changes could be lost before they reach Athena, with
|
|
312
|
+
* the copy it offers of them (Word's "Upload pending" and "Changes not saved"), by
|
|
313
|
+
* {@link ChangesAtRisk}.
|
|
314
|
+
*/
|
|
315
|
+
export declare function changesAtRiskCopy({ risk, device, }: {
|
|
316
|
+
risk: ChangesAtRisk;
|
|
317
|
+
device: DeviceNoun;
|
|
318
|
+
}): StatusCopy & {
|
|
319
|
+
download: string;
|
|
320
|
+
downloadFailed: string;
|
|
321
|
+
};
|
|
322
|
+
/**
|
|
323
|
+
* The action that asks the device to keep what it holds until it syncs, where asking shows the
|
|
324
|
+
* person a question of the device's own (Firefox asks whether the site may store data in
|
|
325
|
+
* persistent storage): the app says why first (`changesAtRiskCopy`) and asks only on this press.
|
|
326
|
+
*/
|
|
327
|
+
export declare function keepOnDeviceAction({ device }: {
|
|
328
|
+
device: DeviceNoun;
|
|
329
|
+
}): string;
|
|
330
|
+
/** A browser whose own way of keeping a site's data longer the words point to. */
|
|
331
|
+
export type SaferStorageBrowser = 'safari-mac' | 'safari-ios';
|
|
332
|
+
/**
|
|
333
|
+
* Where changes made offline in Safari are kept longer: Safari keeps a site added to the Dock or
|
|
334
|
+
* the Home Screen as an app of its own, which it is likelier to let keep its data, and the desktop
|
|
335
|
+
* app keeps them in storage of its own.
|
|
336
|
+
*/
|
|
337
|
+
export declare function saferStorageHint({ browser }: {
|
|
338
|
+
browser: SaferStorageBrowser;
|
|
339
|
+
}): string;
|
|
340
|
+
/**
|
|
341
|
+
* What the app says when it finds that changes it kept on the device never reached Athena and the
|
|
342
|
+
* device no longer has them (the browser removed them before they synced), naming how many
|
|
343
|
+
* documents; each document's row says when its changes were made (`lostChangesItemCopy`).
|
|
344
|
+
*/
|
|
345
|
+
export declare function lostChangesCopy({ device, documents, }: {
|
|
346
|
+
device: DeviceNoun;
|
|
347
|
+
documents: number;
|
|
348
|
+
}): StatusCopy & {
|
|
349
|
+
why: string;
|
|
350
|
+
open: string;
|
|
351
|
+
dismiss: string;
|
|
352
|
+
};
|
|
353
|
+
/** One document's row under `lostChangesCopy`: the words before when its changes were made. */
|
|
354
|
+
export declare function lostChangesItemCopy({ device }: {
|
|
355
|
+
device: DeviceNoun;
|
|
356
|
+
}): string;
|
|
256
357
|
/**
|
|
257
358
|
* The summary heading of the one "Waiting to sync" list, from what waits on
|
|
258
359
|
* the device; `null` when nothing does.
|
|
@@ -288,10 +389,15 @@ export type WaitingItemState =
|
|
|
288
389
|
| 'needs-you'
|
|
289
390
|
/** Athena didn't take it: the app adds why, and what the person can do about it. */
|
|
290
391
|
| 'not-synced';
|
|
291
|
-
/**
|
|
292
|
-
|
|
392
|
+
/**
|
|
393
|
+
* The state of one item in the "Waiting to sync" list, and the line that explains it. For an item
|
|
394
|
+
* saved on the device (`waiting`, `when-opened`), `evictable` says the device may remove it before
|
|
395
|
+
* it syncs (default `false`).
|
|
396
|
+
*/
|
|
397
|
+
export declare function waitingItemCopy({ state, device, evictable, }: {
|
|
293
398
|
state: WaitingItemState;
|
|
294
399
|
device: DeviceNoun;
|
|
400
|
+
evictable?: boolean;
|
|
295
401
|
}): LabelCopy;
|
|
296
402
|
/** The headings of the "Waiting to sync" list, one per kind of item. */
|
|
297
403
|
export declare const WAITING_SECTION_TITLES: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"offline-status.js","sources":["../../../../agent-runtime/src/offline-status.ts"],"sourcesContent":["/**\n * Athena's offline language: the one set of states and words every app uses to\n * say where the person stands when the network, or Athena, is out of reach —\n * Olympus, the desktop app, the phone app, the Chrome extension and the Office\n * add-in.\n *\n * The rule the words follow: say what still works before what doesn't. Work\n * done while Athena can't be reached is kept on the device and syncs when\n * Athena is back, so every message leads with that promise. Nothing here is\n * shown while Athena answers and nothing waits; the \"back\" messages are brief.\n *\n * Pure strings and small functions, no UI library: each app draws them its own\n * way (a banner, a header chip, a tooltip, a sheet). Reachability itself is\n * each app's own (the browser's or OS's online flag AND agora answering its\n * health read); this module only names the result.\n */\n\n/** Where the app stands with Athena. */\nexport type ConnectionState =\n /** Athena answers. Nothing is shown. */\n | 'online'\n /** The device has no network. */\n | 'offline'\n /** The network is up but Athena doesn't answer. */\n | 'unreachable'\n /** Athena answers again and what waited is being sent. */\n | 'reconnecting'\n /** Just back, and everything that waited was sent. Shown briefly. */\n | 'synced';\n\n/** Where one document's edits stand, for the chip in its header. */\nexport type DocumentSyncState =\n /** In Athena. */\n | 'saved'\n /** Being sent to Athena. */\n | 'saving'\n /** Kept on this device while Athena can't be reached; syncs when it is back. */\n | 'on-device'\n /** Kept only while this tab or app stays open (no device storage); syncs when Athena is back. */\n | 'in-memory'\n /** Sending what was kept on this device. */\n | 'syncing'\n /** Some changes overlap with Athena's version: the person chooses. */\n | 'needs-review'\n /** The document can no longer take these edits (access or state changed): kept, not sent. */\n | 'view-only'\n /**\n * Athena answers but isn't taking this document's changes for now: it holds the document while it\n * repairs it, or hasn't confirmed the latest changes for longer than usual (`SavingDelayCause`\n * says which). Kept, and saved once Athena takes them. Never said while Athena can't be reached:\n * that is `on-device` or `in-memory`.\n */\n | 'delayed';\n\n/** Why a document's saving is delayed while Athena answers (`DocumentSyncState` `delayed`). */\nexport type SavingDelayCause =\n /** Athena holds the document while it repairs it, and takes no changes to it until then. */\n | 'repairing'\n /** Athena hasn't confirmed the latest changes for longer than usual. */\n | 'unconfirmed';\n\n/** How a sentence names the device the person is on. */\nexport type DeviceNoun =\n | 'this browser'\n | 'this Mac'\n | 'this computer'\n | 'this phone'\n | 'this tablet'\n | 'this device';\n\n/**\n * What the app really keeps of work done while Athena can't be reached, so the\n * words never promise more: `device` (stored on the device, survives closing),\n * `memory` (kept only while this tab or app stays open: a tenant that turns\n * device copies off, or storage the app can't open), `none` (a surface that\n * keeps no work offline at all, like the Chrome side panel).\n */\nexport type OfflineKeeping = 'device' | 'memory' | 'none';\n\n/** A title and the one line under it (a banner, a sheet, a tooltip's heading). */\nexport interface StatusCopy {\n title: string;\n detail: string;\n}\n\n/** A short label for a chip or a disabled control, and its one-line explanation. */\nexport interface LabelCopy {\n label: string;\n detail: string;\n}\n\nfunction plural(count: number, one: string, many: string): string {\n return `${count} ${count === 1 ? one : many}`;\n}\n\n/** What a tab or an app is called when work lasts only while it stays open. */\nfunction openContainer(device: DeviceNoun): string {\n return device === 'this browser' ? 'this tab' : 'the app';\n}\n\n/** The promise after \"You're offline\" / \"Can't reach Athena\", honest to what is kept. */\nfunction keepingPromise({\n keeping,\n device,\n when,\n}: {\n keeping: OfflineKeeping;\n device: DeviceNoun;\n when: string;\n}): string {\n switch (keeping) {\n case 'device':\n return `You can keep working. Changes are saved on ${device} and sync ${when}.`;\n case 'memory':\n return `You can keep working, but changes are kept only until you close ${openContainer(device)}. They sync ${when}.`;\n case 'none':\n return `You’re still signed in. This reconnects ${when}.`;\n }\n}\n\n/**\n * The app-wide status line, or `null` while Athena answers and nothing waits.\n * `waiting` counts changes not in Athena yet: while `reconnecting` they are\n * being sent; while `online` they wait for something else (a copy that sends\n * when it is next opened), and the line stays until they are gone.\n * `keeping` is what the app really keeps offline (default `device`). A surface\n * that keeps nothing (`none`) has nothing to sync, so its way back is only\n * \"Back online\", never \"Syncing…\" or \"All changes synced\".\n */\nexport function connectionCopy({\n state,\n device,\n waiting = 0,\n keeping = 'device',\n}: {\n state: ConnectionState;\n device: DeviceNoun;\n waiting?: number;\n keeping?: OfflineKeeping;\n}): StatusCopy | null {\n switch (state) {\n case 'online':\n return waiting > 0\n ? {\n title: 'Not synced yet',\n detail: `${plural(waiting, 'change', 'changes')} on ${device} ${waiting === 1 ? 'hasn’t' : 'haven’t'} synced yet.`,\n }\n : null;\n case 'offline':\n return {\n title: 'You’re offline',\n detail: keepingPromise({ keeping, device, when: 'when you’re back online' }),\n };\n case 'unreachable':\n return {\n title: 'Can’t reach Athena',\n detail: keepingPromise({ keeping, device, when: 'when Athena is back' }),\n };\n case 'reconnecting':\n if (keeping === 'none') return { title: 'Back online', detail: '' };\n return {\n title: 'Back online',\n detail:\n waiting > 0 ? `Syncing ${plural(waiting, 'change', 'changes')}…` : 'Syncing…',\n };\n case 'synced':\n return keeping === 'none'\n ? { title: 'Back online', detail: '' }\n : { title: 'All changes synced', detail: '' };\n }\n}\n\n/**\n * The line under \"Saving delayed\": why saving waits, and what is kept meanwhile, never promising\n * more than the app keeps.\n */\nfunction savingDelayedDetail({\n delay,\n keeping,\n device,\n}: {\n delay: SavingDelayCause;\n keeping: Exclude<OfflineKeeping, 'none'>;\n device: DeviceNoun;\n}): string {\n if (delay === 'repairing') {\n return keeping === 'memory'\n ? `You can keep working, but your changes are kept only until you close ${openContainer(device)}. They save once Athena finishes repairing this document.`\n : `You can keep working. Your changes are kept on ${device} and save once Athena finishes repairing this document.`;\n }\n return keeping === 'memory'\n ? `Athena is taking longer than usual to save your latest changes. They’re kept only until you close ${openContainer(device)}, so keep it open.`\n : `Athena is taking longer than usual to save your latest changes. They’re kept on ${device} in the meantime.`;\n}\n\n/**\n * What `on-device` says. The desktop app on a Mac speaks as Word, Excel and PowerPoint for Mac do\n * of a cloud document whose changes wait on the Mac (Office's own title-bar words): \"Upload\n * Pending\", and \"Upload Pending (Offline)\" while Athena can't be reached. Every other app says the\n * changes are saved on its device: a browser has no such words (Word for the web can't edit\n * offline), nor does the phone.\n */\nfunction onDeviceCopy({ device, reachable }: { device: DeviceNoun; reachable: boolean }): LabelCopy {\n if (device === 'this Mac') {\n return {\n label: reachable ? 'Upload Pending' : 'Upload Pending (Offline)',\n detail: 'Upload pending. Your changes sync when Athena is back.',\n };\n }\n return {\n label: `Saved on ${device}`,\n detail: `Saved on ${device}. Your changes sync when Athena is back.`,\n };\n}\n\n/**\n * The chip in a document's header, and the line it explains itself with. For `delayed`, `delay`\n * says why (default `unconfirmed`) and `keeping` whether the changes are kept on the device or only\n * while the tab or app stays open (default `device`); every other state ignores both. `reachable`\n * (default `true`) is whether Athena can be reached, which only the Mac's `on-device` words read.\n */\nexport function documentSyncCopy({\n state,\n device,\n delay = 'unconfirmed',\n keeping = 'device',\n reachable = true,\n}: {\n state: DocumentSyncState;\n device: DeviceNoun;\n delay?: SavingDelayCause;\n keeping?: Exclude<OfflineKeeping, 'none'>;\n reachable?: boolean;\n}): LabelCopy {\n switch (state) {\n case 'saved':\n return { label: 'Saved', detail: 'Saved to Athena.' };\n case 'saving':\n return { label: 'Saving…', detail: 'Saving to Athena.' };\n case 'on-device':\n return onDeviceCopy({ device, reachable });\n case 'in-memory':\n return {\n label: 'Not saved yet',\n detail: `Your changes are kept only until you close ${openContainer(device)}, and save when Athena is back.`,\n };\n case 'syncing':\n return { label: 'Syncing…', detail: `Sending the changes saved on ${device}.` };\n case 'needs-review':\n return {\n label: 'Review changes',\n detail: 'Some of your changes overlap with changes made in Athena. Choose what to keep.',\n };\n case 'view-only':\n return {\n label: 'View only',\n detail: `You can’t edit this anymore. Your changes stay on ${device}.`,\n };\n case 'delayed':\n return { label: 'Saving delayed', detail: savingDelayedDetail({ delay, keeping, device }) };\n }\n}\n\n/**\n * How long a document's edits may wait unconfirmed on a connection that is up before its saving\n * reads as delayed (`delayed`, `unconfirmed`), and before its room asks Keryx why: the stall window\n * every editor shares (`ACK_STALL_MS` in `@athenaintel/asset-host/local-updates`, which Olympus's\n * `room-sync-store.test.ts` holds equal).\n */\nexport const UNCONFIRMED_EDITS_STALL_MS = 15_000;\n\n/**\n * Whether a document's saving is delayed, and why (`DocumentSyncState` `delayed`): only while Athena\n * answers and the document's room is connected; `repairing` when Keryx refuses the room's writes\n * (quarantined while it is repaired: said at once, before anything waits), `unconfirmed` once its\n * edits have stayed unconfirmed past {@link UNCONFIRMED_EDITS_STALL_MS}; `null` otherwise. Every\n * app's chip asks it after a hold and after edits kept without a connection, and ahead of syncing\n * and saving, so a room that refuses writes never spins and being offline is never a delay.\n */\nexport function savingDelayCause({\n reachable,\n connected,\n writesRefused,\n unconfirmedStalled,\n}: {\n /** Athena answers (the app's reachability, never the socket alone). */\n reachable: boolean;\n /** The document's room is connected. */\n connected: boolean;\n /** Keryx's document status says the room takes no writes for now. */\n writesRefused: boolean;\n /** Local edits have waited unconfirmed past the stall window on this connection. */\n unconfirmedStalled: boolean;\n}): SavingDelayCause | null {\n if (!reachable || !connected) return null;\n if (writesRefused) return 'repairing';\n return unconfirmedStalled ? 'unconfirmed' : null;\n}\n\n/**\n * What the person lost of a document while it was open, when the server checked its room's grant\n * again (Keryx's 4401, after a share changed): `edit`, they may still view it but no longer edit it;\n * `view`, they may no longer open it at all.\n */\nexport type AccessLoss = 'edit' | 'view';\n\n/** What a document says once the person's access changed while it was open. */\nexport interface AccessChangedCopy extends StatusCopy {\n /**\n * The label of the button that saves the person's version as a file of their own, with the\n * changes Athena never took, or `null` when nothing is offered: nothing of theirs waits, or the\n * device no longer serves the document (`view`).\n */\n saveCopy: string | null;\n}\n\n/**\n * What a document says once the person's access to it changed while it was open (its editor turns\n * read-only at once), in Word's own words (Word for Mac's message bar, `strman_a6d4`, and its\n * \"Permissions Have Changed\" notice, `strman_0a3e`). `keptChanges`: changes Athena never took wait\n * on the device, never sent; Word offers to save a copy of the file so nothing typed is lost. A\n * `view` loss offers nothing to save: the device stops serving a document the person may no longer\n * read, and their changes stay where they are kept.\n */\nexport function accessChangedCopy({\n loss,\n keptChanges,\n}: {\n loss: AccessLoss;\n keptChanges: boolean;\n}): AccessChangedCopy {\n if (loss === 'view') {\n return {\n title: 'Permissions Have Changed',\n detail:\n 'The permissions on this file were updated and you no longer have rights to view this file. If you previously made changes to this file, they may not be saved.',\n saveCopy: null,\n };\n }\n if (keptChanges) {\n return {\n title: 'Can’t save',\n detail:\n 'You no longer have the permission to edit this file. To keep your changes, save a copy of the file.',\n saveCopy: 'Save a Copy',\n };\n }\n return {\n title: 'Read Only',\n detail: 'You no longer have the permission to edit this file.',\n saveCopy: null,\n };\n}\n\n/** When something that needs Athena works again (the second half of `NEEDS_CONNECTION`'s line). */\nconst AVAILABLE_AGAIN = 'It’s available again when you’re back online.';\n\n/**\n * The reason on a control that needs Athena (share, run, upload, invite…),\n * shown disabled while Athena can't be reached rather than failing when used.\n */\nexport const NEEDS_CONNECTION: LabelCopy = {\n label: 'Needs a connection',\n detail: `This needs Athena. ${AVAILABLE_AGAIN}`,\n};\n\n/** What a composer was given to attach: a photo (from the phone's library or camera) or any file. */\nexport type AttachmentKind = 'photo' | 'file';\n\n/**\n * What a composer says when a photo or file is picked, pasted or dropped while\n * Athena can't be reached. It isn't attached: only Athena can take the upload.\n * In a chat whose messages wait to be sent meanwhile (`messagesWait`: the chat\n * keeps an outbox), the message itself still goes, so that comes first. Where\n * nothing waits (the Chrome side panel, a launcher that keeps its draft), it\n * says only when attaching works again, as `NEEDS_CONNECTION` does.\n */\nexport function attachmentNeedsConnection({\n kind,\n messagesWait,\n}: {\n kind: AttachmentKind;\n messagesWait: boolean;\n}): string {\n const attaching = `Attaching ${kind === 'photo' ? 'a photo' : 'a file'} needs Athena. ${AVAILABLE_AGAIN}`;\n return messagesWait\n ? `You can still send your message: it goes when Athena is back. ${attaching}`\n : attaching;\n}\n\n/** What a sent message carries besides its text that can't wait for Athena. */\nfunction heldExtras(withQuote: boolean): string {\n return withQuote ? 'an attachment or a quote' : 'an attachment';\n}\n\n/**\n * What a composer says when a message is sent while Athena can't be reached\n * with an attachment that never reached Athena (or, `withQuote`, a quote the\n * chat can't keep waiting). Its text could wait in the chat outbox; the\n * attachment can't, so the draft stays as it is.\n */\nexport function attachedMessageNeedsConnection({\n withQuote = false,\n}: {\n withQuote?: boolean;\n} = {}): string {\n return `Your text can wait and go when Athena is back, but ${heldExtras(withQuote)} can’t. Remove ${\n withQuote ? 'it' : 'the attachment'\n } to send the text.`;\n}\n\n/**\n * What a composer says when a message with an attachment (or, `withQuote`, a\n * quote) is sent while messages written when Athena couldn't be reached still\n * wait to go: those go first, and this one can't wait behind them.\n */\nexport function attachedMessageBehindWaiting({\n withQuote = false,\n}: {\n withQuote?: boolean;\n} = {}): string {\n return `Messages you wrote while Athena couldn’t be reached go first. A message with ${heldExtras(\n withQuote\n )} can’t wait behind them, so send it once they’ve gone.`;\n}\n\n/**\n * The reason on New Presentation while Athena can't be reached, in a workspace\n * whose new presentations start from its design system's template: a\n * presentation made offline starts blank, and only Athena can copy the\n * template, so none is made until it can.\n */\nexport const NEW_PRESENTATION_NEEDS_TEMPLATE: LabelCopy = {\n label: 'Needs a connection',\n detail:\n 'New presentations in this workspace start from its design template, which needs Athena. You can make one when you’re back online.',\n};\n\n/**\n * What a download of a spreadsheet made from an Excel Macro-Enabled Workbook\n * (`.xlsm`) says when it went without the macros because they come from Athena,\n * which can't be reached, and the device hasn't kept them (they travel with the\n * workbook and never run). The file is the workbook without them; downloading\n * it again with a connection keeps them.\n */\nexport function downloadedWithoutMacros({ device }: { device: DeviceNoun }): string {\n return `Downloaded without the spreadsheet’s macros: they come from Athena, which can’t be reached, and ${device} hasn’t kept them. Download it again when you’re back online to keep them.`;\n}\n\n/**\n * Save's question for a spreadsheet whose file is an Excel Macro-Enabled\n * Workbook (`.xlsm`) while its macros come from Athena, which can't be reached,\n * and the device hasn't kept them: the `.xlsm` is never written without them,\n * so the person may save an Excel Workbook (`.xlsx`) beside it, and Save writes\n * the `.xlsm` again once Athena answers.\n */\nexport function macrosNeedConnectionToSave({\n name,\n device,\n}: {\n name: string;\n device: DeviceNoun;\n}): StatusCopy {\n return {\n title: `Athena can’t save the macros in “${name}” right now.`,\n detail: `They come from Athena, which can’t be reached, and ${device} hasn’t kept them. Save it as an Excel Workbook (.xlsx) instead. “${name}” stays as it is, macros included, and Save writes it again when you’re back online.`,\n };\n}\n\n/**\n * What saving a version kept on the device as a new spreadsheet says when the\n * spreadsheet was made from an Excel Macro-Enabled Workbook (`.xlsm`) and\n * Athena can't be reached: the new spreadsheet is made all the same (on the\n * device, and it syncs when Athena is back), but without the macros, which only\n * Athena can keep with a new spreadsheet (they travel with the workbook and\n * never run). The kept version stays on the device, so saving it again with a\n * connection keeps them.\n */\nexport function savedAsNewWithoutMacros({ device }: { device: DeviceNoun }): string {\n return `The new spreadsheet was made without the macros: only Athena can keep them in a new spreadsheet, and it can’t be reached. The kept version stays on ${device}: save it as a new spreadsheet again when you’re back online to keep them.`;\n}\n\n/**\n * Whether an item opens without a connection on this device, for lists.\n * `canKeep` says whether opening it online would keep a copy here (the\n * deployment allows device copies and the item's type is one that is kept):\n * only then is the person told to open it once.\n */\nexport function offlineAvailabilityCopy({\n available,\n canKeep,\n device,\n}: {\n available: boolean;\n canKeep: boolean;\n device: DeviceNoun;\n}): LabelCopy {\n if (available) {\n return { label: 'Available offline', detail: `Opens without a connection on ${device}.` };\n }\n return canKeep\n ? {\n label: 'Not available offline',\n detail: `Open it once while online to keep it on ${device}.`,\n }\n : { label: 'Not available offline', detail: 'This opens only with a connection.' };\n}\n\n/**\n * The one-time explainer shown the first time the person goes offline, on a\n * surface that keeps work there (`keeping` `device`). A surface that keeps\n * nothing (`none`: the Chrome side panel, the Office task pane) shows no\n * explainer; its status line already says all that holds.\n */\nexport function firstOfflineExplainer({ device }: { device: DeviceNoun }): StatusCopy & {\n action: string;\n} {\n return {\n title: 'You can keep working offline',\n detail: `Documents you’ve opened recently keep working on ${device}. Your changes sync when you’re back online.`,\n action: 'Got it',\n };\n}\n\n/**\n * The summary heading of the one \"Waiting to sync\" list, from what waits on\n * the device; `null` when nothing does.\n */\nexport function waitingSummary({\n changes = 0,\n messages = 0,\n recordings = 0,\n}: {\n changes?: number;\n messages?: number;\n recordings?: number;\n}): string | null {\n const parts = [\n changes > 0 ? plural(changes, 'change', 'changes') : null,\n messages > 0 ? plural(messages, 'message', 'messages') : null,\n recordings > 0 ? plural(recordings, 'recording', 'recordings') : null,\n ].filter((part): part is string => part !== null);\n return parts.length === 0 ? null : `Waiting to sync: ${parts.join(', ')}`;\n}\n\n/**\n * Where one item in the \"Waiting to sync\" list stands: a change, a message or a\n * recording made while Athena couldn't be reached.\n */\nexport type WaitingItemState =\n /** Saved on the device; it syncs when Athena is back. */\n | 'waiting'\n /** Being sent to Athena now. */\n | 'syncing'\n /**\n * Kept only while this tab or app stays open: it couldn't be saved on the\n * device (the organization keeps nothing on devices, or storage failed).\n */\n | 'in-memory'\n /**\n * Saved on the device in something that isn't open (a document closed before\n * its edits synced, one made offline): it syncs when it is next opened with a\n * connection (opening it offline sends nothing).\n */\n | 'when-opened'\n /** Made in another workspace: it syncs once that workspace is open again, with a connection. */\n | 'other-workspace'\n /** It doesn't go on its own: the person chooses (a message that waited more than a day). */\n | 'needs-you'\n /** Athena didn't take it: the app adds why, and what the person can do about it. */\n | 'not-synced';\n\n/** The state of one item in the \"Waiting to sync\" list, and the line that explains it. */\nexport function waitingItemCopy({\n state,\n device,\n}: {\n state: WaitingItemState;\n device: DeviceNoun;\n}): LabelCopy {\n switch (state) {\n case 'waiting':\n return {\n label: 'Waiting to sync',\n detail: `Saved on ${device}. It syncs when Athena is back.`,\n };\n case 'syncing':\n return { label: 'Syncing…', detail: `Sending what was saved on ${device}.` };\n case 'in-memory':\n return {\n label: 'Not saved yet',\n detail: `Kept only until you close ${openContainer(device)}. It syncs when Athena is back.`,\n };\n case 'when-opened':\n return {\n label: 'Not synced yet',\n detail: `Saved on ${device}. It syncs when you next open it with a connection.`,\n };\n case 'other-workspace':\n return {\n label: 'Waiting for its workspace',\n detail: 'It syncs once you’re in the workspace it was made in, with a connection.',\n };\n case 'needs-you':\n return { label: 'Waiting for you', detail: 'Open it to choose what happens to it.' };\n case 'not-synced':\n return { label: 'Couldn’t sync', detail: 'Athena didn’t take it.' };\n }\n}\n\n/** The headings of the \"Waiting to sync\" list, one per kind of item. */\nexport const WAITING_SECTION_TITLES = {\n changes: 'Changes',\n messages: 'Messages',\n recordings: 'Recordings',\n} as const;\n\n/**\n * What the person can do with an item Athena didn't take (`not-synced`) in the\n * \"Waiting to sync\" list: send it again, or delete the copy on the device. That\n * copy is the item's only one, so the app asks first (`deleteFromDeviceConfirm`)\n * and never deletes it as a side effect of anything else.\n */\nexport function notSyncedActions({ device }: { device: DeviceNoun }): {\n retry: string;\n remove: string;\n} {\n return { retry: 'Try again', remove: `Delete from ${device}` };\n}\n\n/** One of `notSyncedActions`, or showing where the item is kept on the device. */\nexport type NotSyncedAction = 'retry' | 'reveal' | 'remove';\n\n/**\n * What the person hears when the device couldn't do what they asked for an\n * item Athena didn't take (its files, not Athena, failed).\n */\nexport function notSyncedActionFailed({\n action,\n device,\n}: {\n action: NotSyncedAction;\n device: DeviceNoun;\n}): string {\n switch (action) {\n case 'retry':\n return 'Couldn’t send it again. Try again in a moment.';\n case 'reveal':\n return `Couldn’t show where it’s kept on ${device}.`;\n case 'remove':\n return `Couldn’t delete it from ${device}.`;\n }\n}\n\n/**\n * The question before deleting such an item's copy on the device, naming the\n * item: the destructive choice is never the default.\n */\nexport function deleteFromDeviceConfirm({\n name,\n device,\n}: {\n name: string;\n device: DeviceNoun;\n}): StatusCopy & { confirm: string; cancel: string } {\n return {\n title: `Delete “${name}” from ${device}?`,\n detail: 'Athena didn’t take it, so this is its only copy. Deleting it can’t be undone.',\n confirm: 'Delete',\n cancel: 'Cancel',\n };\n}\n"],"names":[],"mappings":"AA6QO,MAAM,6BAA6B;AAsF1C,MAAM,kBAAkB;AAMjB,MAAM,mBAA8B;AAAA,EACzC,OAAO;AAAA,EACP,QAAQ,sBAAsB,eAAe;AAC/C;AAaO,SAAS,0BAA0B;AAAA,EACxC;AAAA,EACA;AACF,GAGW;AACT,QAAM,YAAY,aAA4C,QAAQ,kBAAkB,eAAe;AACvG,SAAO,eACH,iEAAiE,SAAS,KAC1E;AACN;AAGA,SAAS,WAAW,WAA4B;AAC9C,SAAO,YAAY,6BAA6B;AAClD;AAQO,SAAS,+BAA+B;AAAA,EAC7C,YAAY;AACd,IAEI,IAAY;AACd,SAAO,sDAAsD,WAAW,SAAS,CAAC,kBAChF,YAAY,OAAO,gBACrB;AACF;AAOO,SAAS,6BAA6B;AAAA,EAC3C,YAAY;AACd,IAEI,IAAY;AACd,SAAO,gFAAgF;AAAA,IACrF;AAAA,EAAA,CACD;AACH;"}
|
|
1
|
+
{"version":3,"file":"offline-status.js","sources":["../../../../agent-runtime/src/offline-status.ts"],"sourcesContent":["/**\n * Athena's offline language: the one set of states and words every app uses to\n * say where the person stands when the network, or Athena, is out of reach —\n * Olympus, the desktop app, the phone app, the Chrome extension and the Office\n * add-in.\n *\n * The rule the words follow: say what still works before what doesn't. Work\n * done while Athena can't be reached is kept on the device and syncs when\n * Athena is back, so every message leads with that promise. Nothing here is\n * shown while Athena answers and nothing waits; the \"back\" messages are brief.\n *\n * Pure strings and small functions, no UI library: each app draws them its own\n * way (a banner, a header chip, a tooltip, a sheet). Reachability itself is\n * each app's own (the browser's or OS's online flag AND agora answering its\n * health read); this module only names the result.\n */\n\n/** Where the app stands with Athena. */\nexport type ConnectionState =\n /** Athena answers. Nothing is shown. */\n | 'online'\n /** The device has no network. */\n | 'offline'\n /** The network is up but Athena doesn't answer. */\n | 'unreachable'\n /** Athena answers again and what waited is being sent. */\n | 'reconnecting'\n /** Just back, and everything that waited was sent. Shown briefly. */\n | 'synced';\n\n/** Where one document's edits stand, for the chip in its header. */\nexport type DocumentSyncState =\n /** In Athena. */\n | 'saved'\n /** Being sent to Athena. */\n | 'saving'\n /** Kept on this device while Athena can't be reached; syncs when it is back. */\n | 'on-device'\n /** Kept only while this tab or app stays open (no device storage); syncs when Athena is back. */\n | 'in-memory'\n /** Sending what was kept on this device. */\n | 'syncing'\n /** Some changes overlap with Athena's version: the person chooses. */\n | 'needs-review'\n /** The document can no longer take these edits (access or state changed): kept, not sent. */\n | 'view-only'\n /**\n * Athena answers but isn't taking this document's changes for now: it holds the document while it\n * repairs it, or hasn't confirmed the latest changes for longer than usual (`SavingDelayCause`\n * says which). Kept, and saved once Athena takes them. Never said while Athena can't be reached:\n * that is `on-device` or `in-memory`.\n */\n | 'delayed';\n\n/** Why a document's saving is delayed while Athena answers (`DocumentSyncState` `delayed`). */\nexport type SavingDelayCause =\n /** Athena holds the document while it repairs it, and takes no changes to it until then. */\n | 'repairing'\n /** Athena hasn't confirmed the latest changes for longer than usual. */\n | 'unconfirmed';\n\n/** How a sentence names the device the person is on. */\nexport type DeviceNoun =\n | 'this browser'\n | 'this Mac'\n | 'this computer'\n | 'this phone'\n | 'this tablet'\n | 'this device';\n\n/**\n * What the app really keeps of work done while Athena can't be reached, so the\n * words never promise more: `device` (stored on the device, survives closing),\n * `memory` (kept only while this tab or app stays open: a tenant that turns\n * device copies off, or storage the app can't open), `none` (a surface that\n * keeps no work offline at all, like the Chrome side panel).\n */\nexport type OfflineKeeping = 'device' | 'memory' | 'none';\n\n/** A title and the one line under it (a banner, a sheet, a tooltip's heading). */\nexport interface StatusCopy {\n title: string;\n detail: string;\n}\n\n/** A short label for a chip or a disabled control, and its one-line explanation. */\nexport interface LabelCopy {\n label: string;\n detail: string;\n}\n\nfunction plural(count: number, one: string, many: string): string {\n return `${count} ${count === 1 ? one : many}`;\n}\n\n/** What a tab or an app is called when work lasts only while it stays open. */\nfunction openContainer(device: DeviceNoun): string {\n return device === 'this browser' ? 'this tab' : 'the app';\n}\n\n/** The device noun at the start of a sentence (\"This browser can…\"). */\nfunction deviceAtStart(device: DeviceNoun): string {\n return `${device.charAt(0).toUpperCase()}${device.slice(1)}`;\n}\n\n/**\n * The sentence a promise ends with when the device can remove what it keeps before it syncs\n * (`evictable`: a browser that hasn't granted persistent storage, a private window): \"before then\"\n * after a sentence that says when it syncs, \"before it syncs\" after one that doesn't.\n */\nfunction removableBeforeSync({\n device,\n count,\n before = 'then',\n}: {\n device: DeviceNoun;\n count: number;\n before?: 'then' | 'syncing';\n}): string {\n const what = count === 1 ? 'it' : 'them';\n const when = before === 'then' ? 'then' : count === 1 ? 'it syncs' : 'they sync';\n return `${deviceAtStart(device)} can remove ${what} before ${when}.`;\n}\n\n/** The promise after \"You're offline\" / \"Can't reach Athena\", honest to what is kept. */\nfunction keepingPromise({\n keeping,\n device,\n when,\n evictable,\n}: {\n keeping: OfflineKeeping;\n device: DeviceNoun;\n when: string;\n evictable: boolean;\n}): string {\n switch (keeping) {\n case 'device':\n return evictable\n ? `You can keep working. Changes are kept on ${device} and sync ${when}. ${removableBeforeSync({ device, count: 2 })}`\n : `You can keep working. Changes are saved on ${device} and sync ${when}.`;\n case 'memory':\n return `You can keep working, but changes are kept only until you close ${openContainer(device)}. They sync ${when}.`;\n case 'none':\n return `You’re still signed in. This reconnects ${when}.`;\n }\n}\n\n/**\n * The app-wide status line, or `null` while Athena answers and nothing waits.\n * `waiting` counts changes not in Athena yet: while `reconnecting` they are\n * being sent; while `online` they wait for something else (a copy that sends\n * when it is next opened), and the line stays until they are gone.\n * `keeping` is what the app really keeps offline (default `device`). A surface\n * that keeps nothing (`none`) has nothing to sync, so its way back is only\n * \"Back online\", never \"Syncing…\" or \"All changes synced\".\n * `evictable`: the device may remove what it keeps before it syncs (a browser that hasn't granted\n * persistent storage, a private window); only `keeping` `device` says so (default `false`).\n */\nexport function connectionCopy({\n state,\n device,\n waiting = 0,\n keeping = 'device',\n evictable = false,\n}: {\n state: ConnectionState;\n device: DeviceNoun;\n waiting?: number;\n keeping?: OfflineKeeping;\n evictable?: boolean;\n}): StatusCopy | null {\n const atRisk = evictable && keeping === 'device';\n switch (state) {\n case 'online':\n return waiting > 0\n ? {\n title: 'Not synced yet',\n detail: `${plural(waiting, 'change', 'changes')} on ${device} ${waiting === 1 ? 'hasn’t' : 'haven’t'} synced yet.${\n atRisk ? ` ${removableBeforeSync({ device, count: waiting, before: 'syncing' })}` : ''\n }`,\n }\n : null;\n case 'offline':\n return {\n title: 'You’re offline',\n detail: keepingPromise({\n keeping,\n device,\n when: 'when you’re back online',\n evictable: atRisk,\n }),\n };\n case 'unreachable':\n return {\n title: 'Can’t reach Athena',\n detail: keepingPromise({ keeping, device, when: 'when Athena is back', evictable: atRisk }),\n };\n case 'reconnecting':\n if (keeping === 'none') return { title: 'Back online', detail: '' };\n return {\n title: 'Back online',\n detail:\n waiting > 0 ? `Syncing ${plural(waiting, 'change', 'changes')}…` : 'Syncing…',\n };\n case 'synced':\n return keeping === 'none'\n ? { title: 'Back online', detail: '' }\n : { title: 'All changes synced', detail: '' };\n }\n}\n\n/**\n * The line under \"Saving delayed\": why saving waits, and what is kept meanwhile, never promising\n * more than the app keeps.\n */\nfunction savingDelayedDetail({\n delay,\n keeping,\n device,\n}: {\n delay: SavingDelayCause;\n keeping: Exclude<OfflineKeeping, 'none'>;\n device: DeviceNoun;\n}): string {\n if (delay === 'repairing') {\n return keeping === 'memory'\n ? `You can keep working, but your changes are kept only until you close ${openContainer(device)}. They save once Athena finishes repairing this document.`\n : `You can keep working. Your changes are kept on ${device} and save once Athena finishes repairing this document.`;\n }\n return keeping === 'memory'\n ? `Athena is taking longer than usual to save your latest changes. They’re kept only until you close ${openContainer(device)}, so keep it open.`\n : `Athena is taking longer than usual to save your latest changes. They’re kept on ${device} in the meantime.`;\n}\n\n/**\n * What `on-device` says. The desktop app on a Mac speaks as Word, Excel and PowerPoint for Mac do\n * of a cloud document whose changes wait on the Mac (Office's own title-bar words): \"Upload\n * Pending\", and \"Upload Pending (Offline)\" while Athena can't be reached. Every other app says the\n * changes are saved on its device: a browser has no such words (Word for the web can't edit\n * offline), nor does the phone.\n */\nfunction onDeviceCopy({ device, reachable }: { device: DeviceNoun; reachable: boolean }): LabelCopy {\n if (device === 'this Mac') {\n return {\n label: reachable ? 'Upload Pending' : 'Upload Pending (Offline)',\n detail: 'Upload pending. Your changes sync when Athena is back.',\n };\n }\n return {\n label: `Saved on ${device}`,\n detail: `Saved on ${device}. Your changes sync when Athena is back.`,\n };\n}\n\n/**\n * The chip in a document's header, and the line it explains itself with. For `delayed`, `delay`\n * says why (default `unconfirmed`) and `keeping` whether the changes are kept on the device or only\n * while the tab or app stays open (default `device`); every other state ignores both. `reachable`\n * (default `true`) is whether Athena can be reached, which only the Mac's `on-device` words read.\n * For `on-device`, `evictable` says the device may remove the changes before they sync (default\n * `false`).\n */\nexport function documentSyncCopy({\n state,\n device,\n delay = 'unconfirmed',\n keeping = 'device',\n reachable = true,\n evictable = false,\n}: {\n state: DocumentSyncState;\n device: DeviceNoun;\n delay?: SavingDelayCause;\n keeping?: Exclude<OfflineKeeping, 'none'>;\n reachable?: boolean;\n evictable?: boolean;\n}): LabelCopy {\n switch (state) {\n case 'saved':\n return { label: 'Saved', detail: 'Saved to Athena.' };\n case 'saving':\n return { label: 'Saving…', detail: 'Saving to Athena.' };\n case 'on-device': {\n const copy = onDeviceCopy({ device, reachable });\n return evictable\n ? { ...copy, detail: `${copy.detail} ${removableBeforeSync({ device, count: 2 })}` }\n : copy;\n }\n case 'in-memory':\n return {\n label: 'Not saved yet',\n detail: `Your changes are kept only until you close ${openContainer(device)}, and save when Athena is back.`,\n };\n case 'syncing':\n return { label: 'Syncing…', detail: `Sending the changes saved on ${device}.` };\n case 'needs-review':\n return {\n label: 'Review changes',\n detail: 'Some of your changes overlap with changes made in Athena. Choose what to keep.',\n };\n case 'view-only':\n return {\n label: 'View only',\n detail: `You can’t edit this anymore. Your changes stay on ${device}.`,\n };\n case 'delayed':\n return { label: 'Saving delayed', detail: savingDelayedDetail({ delay, keeping, device }) };\n }\n}\n\n/**\n * How long a document's edits may wait unconfirmed on a connection that is up before its saving\n * reads as delayed (`delayed`, `unconfirmed`), and before its room asks Keryx why: the stall window\n * every editor shares (`ACK_STALL_MS` in `@athenaintel/asset-host/local-updates`, which Olympus's\n * `room-sync-store.test.ts` holds equal).\n */\nexport const UNCONFIRMED_EDITS_STALL_MS = 15_000;\n\n/**\n * Whether a document's saving is delayed, and why (`DocumentSyncState` `delayed`): only while Athena\n * answers and the document's room is connected; `repairing` when Keryx refuses the room's writes\n * (quarantined while it is repaired: said at once, before anything waits), `unconfirmed` once its\n * edits have stayed unconfirmed past {@link UNCONFIRMED_EDITS_STALL_MS}; `null` otherwise. Every\n * app's chip asks it after a hold and after edits kept without a connection, and ahead of syncing\n * and saving, so a room that refuses writes never spins and being offline is never a delay.\n */\nexport function savingDelayCause({\n reachable,\n connected,\n writesRefused,\n unconfirmedStalled,\n}: {\n /** Athena answers (the app's reachability, never the socket alone). */\n reachable: boolean;\n /** The document's room is connected. */\n connected: boolean;\n /** Keryx's document status says the room takes no writes for now. */\n writesRefused: boolean;\n /** Local edits have waited unconfirmed past the stall window on this connection. */\n unconfirmedStalled: boolean;\n}): SavingDelayCause | null {\n if (!reachable || !connected) return null;\n if (writesRefused) return 'repairing';\n return unconfirmedStalled ? 'unconfirmed' : null;\n}\n\n/**\n * What the person lost of a document while it was open, when the server checked its room's grant\n * again (Keryx's 4401, after a share changed): `edit`, they may still view it but no longer edit it;\n * `view`, they may no longer open it at all.\n */\nexport type AccessLoss = 'edit' | 'view';\n\n/** What a document says once the person's access changed while it was open. */\nexport interface AccessChangedCopy extends StatusCopy {\n /**\n * The label of the button that saves the person's version as a file of their own, with the\n * changes Athena never took, or `null` when nothing is offered: nothing of theirs waits, or the\n * device no longer serves the document (`view`).\n */\n saveCopy: string | null;\n}\n\n/**\n * What a document says once the person's access to it changed while it was open (its editor turns\n * read-only at once), in Word's own words (Word for Mac's message bar, `strman_a6d4`, and its\n * \"Permissions Have Changed\" notice, `strman_0a3e`). `keptChanges`: changes Athena never took wait\n * on the device, never sent; Word offers to save a copy of the file so nothing typed is lost. A\n * `view` loss offers nothing to save: the device stops serving a document the person may no longer\n * read, and their changes stay where they are kept.\n */\nexport function accessChangedCopy({\n loss,\n keptChanges,\n}: {\n loss: AccessLoss;\n keptChanges: boolean;\n}): AccessChangedCopy {\n if (loss === 'view') {\n return {\n title: 'Permissions Have Changed',\n detail:\n 'The permissions on this file were updated and you no longer have rights to view this file. If you previously made changes to this file, they may not be saved.',\n saveCopy: null,\n };\n }\n if (keptChanges) {\n return {\n title: 'Can’t save',\n detail:\n 'You no longer have the permission to edit this file. To keep your changes, save a copy of the file.',\n saveCopy: 'Save a Copy',\n };\n }\n return {\n title: 'Read Only',\n detail: 'You no longer have the permission to edit this file.',\n saveCopy: null,\n };\n}\n\n/** What a document says while it holds changes an older version of Athena made on the device. */\nexport interface OlderVersionChangesCopy extends StatusCopy {\n /** Saves this device's version as a file of the person's own, those changes included (Word's). */\n saveCopy: string;\n /** Sets this device's version aside, kept on the device to compare, and opens Athena's. */\n useAthenaVersion: string;\n /** Athena's version couldn't be opened: another window of the device has the document open. */\n openElsewhere: string;\n}\n\n/**\n * What a document says while its local copy holds changes an older version of Athena made on this\n * device, below its room's write floor (`older-version`, `@athenaintel/local-copy/room-copy`): such\n * a version deletes what newer ones store, so those changes are never uploaded. Word's way out,\n * as when it can't save a cloud document (\"You can save a copy to prevent losing your work\",\n * `strman`): Save a Copy, and Athena's version, which keeps this device's version on it to compare\n * and copy from until the person discards it. `saveCopy: false` for an editor that offers no Save\n * a Copy of the live document (the phone's page): its kept version downloads instead.\n */\nexport function olderVersionChangesCopy({\n device,\n noun,\n saveCopy = true,\n}: {\n device: DeviceNoun;\n /** What the person calls the document (\"document\", \"spreadsheet\"). */\n noun: string;\n /** Whether the editor offers Save a Copy beside Athena's version. */\n saveCopy?: boolean;\n}): OlderVersionChangesCopy {\n const made = `You changed this ${noun} on ${device} in an older version of Athena, so those changes can’t be uploaded.`;\n return {\n title: 'Changes made in an older version of Athena',\n detail: saveCopy\n ? `${made} To keep them, save a copy, or use Athena’s version: yours stays on ${device} to compare and copy from.`\n : `${made} Use Athena’s version to keep yours on ${device}, where you can compare it, copy from it or download it.`,\n saveCopy: 'Save a Copy',\n useAthenaVersion: 'Use Athena’s version',\n openElsewhere: `This ${noun} is open in another window. Close it there, then try again.`,\n };\n}\n\n/** When something that needs Athena works again (the second half of `NEEDS_CONNECTION`'s line). */\nconst AVAILABLE_AGAIN = 'It’s available again when you’re back online.';\n\n/**\n * The reason on a control that needs Athena (share, run, upload, invite…),\n * shown disabled while Athena can't be reached rather than failing when used.\n */\nexport const NEEDS_CONNECTION: LabelCopy = {\n label: 'Needs a connection',\n detail: `This needs Athena. ${AVAILABLE_AGAIN}`,\n};\n\n/** What a composer was given to attach: a photo (from the phone's library or camera) or any file. */\nexport type AttachmentKind = 'photo' | 'file';\n\n/**\n * What a composer says when a photo or file is picked, pasted or dropped while\n * Athena can't be reached. It isn't attached: only Athena can take the upload.\n * In a chat whose messages wait to be sent meanwhile (`messagesWait`: the chat\n * keeps an outbox), the message itself still goes, so that comes first. Where\n * nothing waits (the Chrome side panel, a launcher that keeps its draft), it\n * says only when attaching works again, as `NEEDS_CONNECTION` does.\n */\nexport function attachmentNeedsConnection({\n kind,\n messagesWait,\n}: {\n kind: AttachmentKind;\n messagesWait: boolean;\n}): string {\n const attaching = `Attaching ${kind === 'photo' ? 'a photo' : 'a file'} needs Athena. ${AVAILABLE_AGAIN}`;\n return messagesWait\n ? `You can still send your message: it goes when Athena is back. ${attaching}`\n : attaching;\n}\n\n/** What a sent message carries besides its text that can't wait for Athena. */\nfunction heldExtras(withQuote: boolean): string {\n return withQuote ? 'an attachment or a quote' : 'an attachment';\n}\n\n/**\n * What a composer says when a message is sent while Athena can't be reached\n * with an attachment that never reached Athena (or, `withQuote`, a quote the\n * chat can't keep waiting). Its text could wait in the chat outbox; the\n * attachment can't, so the draft stays as it is.\n */\nexport function attachedMessageNeedsConnection({\n withQuote = false,\n}: {\n withQuote?: boolean;\n} = {}): string {\n return `Your text can wait and go when Athena is back, but ${heldExtras(withQuote)} can’t. Remove ${\n withQuote ? 'it' : 'the attachment'\n } to send the text.`;\n}\n\n/**\n * What a composer says when a message with an attachment (or, `withQuote`, a\n * quote) is sent while messages written when Athena couldn't be reached still\n * wait to go: those go first, and this one can't wait behind them.\n */\nexport function attachedMessageBehindWaiting({\n withQuote = false,\n}: {\n withQuote?: boolean;\n} = {}): string {\n return `Messages you wrote while Athena couldn’t be reached go first. A message with ${heldExtras(\n withQuote\n )} can’t wait behind them, so send it once they’ve gone.`;\n}\n\n/**\n * The reason on New Presentation while Athena can't be reached, in a workspace\n * whose new presentations start from its design system's template: a\n * presentation made offline starts blank, and only Athena can copy the\n * template, so none is made until it can.\n */\nexport const NEW_PRESENTATION_NEEDS_TEMPLATE: LabelCopy = {\n label: 'Needs a connection',\n detail:\n 'New presentations in this workspace start from its design template, which needs Athena. You can make one when you’re back online.',\n};\n\n/**\n * What a download of a spreadsheet made from an Excel Macro-Enabled Workbook\n * (`.xlsm`) says when it went without the macros because they come from Athena,\n * which can't be reached, and the device hasn't kept them (they travel with the\n * workbook and never run). The file is the workbook without them; downloading\n * it again with a connection keeps them.\n */\nexport function downloadedWithoutMacros({ device }: { device: DeviceNoun }): string {\n return `Downloaded without the spreadsheet’s macros: they come from Athena, which can’t be reached, and ${device} hasn’t kept them. Download it again when you’re back online to keep them.`;\n}\n\n/**\n * Save's question for a spreadsheet whose file is an Excel Macro-Enabled\n * Workbook (`.xlsm`) while its macros come from Athena, which can't be reached,\n * and the device hasn't kept them: the `.xlsm` is never written without them,\n * so the person may save an Excel Workbook (`.xlsx`) beside it, and Save writes\n * the `.xlsm` again once Athena answers.\n */\nexport function macrosNeedConnectionToSave({\n name,\n device,\n}: {\n name: string;\n device: DeviceNoun;\n}): StatusCopy {\n return {\n title: `Athena can’t save the macros in “${name}” right now.`,\n detail: `They come from Athena, which can’t be reached, and ${device} hasn’t kept them. Save it as an Excel Workbook (.xlsx) instead. “${name}” stays as it is, macros included, and Save writes it again when you’re back online.`,\n };\n}\n\n/**\n * What saving a version kept on the device as a new spreadsheet says when the\n * spreadsheet was made from an Excel Macro-Enabled Workbook (`.xlsm`) and\n * Athena can't be reached: the new spreadsheet is made all the same (on the\n * device, and it syncs when Athena is back), but without the macros, which only\n * Athena can keep with a new spreadsheet (they travel with the workbook and\n * never run). The kept version stays on the device, so saving it again with a\n * connection keeps them.\n */\nexport function savedAsNewWithoutMacros({ device }: { device: DeviceNoun }): string {\n return `The new spreadsheet was made without the macros: only Athena can keep them in a new spreadsheet, and it can’t be reached. The kept version stays on ${device}: save it as a new spreadsheet again when you’re back online to keep them.`;\n}\n\n/**\n * What saving a copy of a Word document says when the copy was made on the\n * device while Athena can't be reached, for a document whose original Word file\n * Athena keeps (one imported from Word): the copy is made all the same, from the\n * document as the device holds it, over the original file kept with it, so it\n * keeps that file's parts but writes its body from what the editor shows.\n * Athena's own copy, made from the original file, keeps everything, so saving\n * a copy again with a connection makes that one.\n */\nexport function wordCopyMadeOnDevice({ device }: { device: DeviceNoun }): StatusCopy {\n return {\n title: `Copy made on ${device}`,\n detail: `Athena can’t be reached, so the copy holds the document as it is on ${device}. Anything in the original Word file that Athena doesn’t show may be missing from it: save a copy again when you’re back online for the complete file.`,\n };\n}\n\n/**\n * Whether an item opens without a connection on this device, for lists.\n * `canKeep` says whether opening it online would keep a copy here (the\n * deployment allows device copies and the item's type is one that is kept):\n * only then is the person told to open it once.\n */\nexport function offlineAvailabilityCopy({\n available,\n canKeep,\n device,\n}: {\n available: boolean;\n canKeep: boolean;\n device: DeviceNoun;\n}): LabelCopy {\n if (available) {\n return { label: 'Available offline', detail: `Opens without a connection on ${device}.` };\n }\n return canKeep\n ? {\n label: 'Not available offline',\n detail: `Open it once while online to keep it on ${device}.`,\n }\n : { label: 'Not available offline', detail: 'This opens only with a connection.' };\n}\n\n/**\n * The one-time explainer shown the first time the person goes offline, on a\n * surface that keeps work there (`keeping` `device`). A surface that keeps\n * nothing (`none`: the Chrome side panel, the Office task pane) shows no\n * explainer; its status line already says all that holds.\n */\nexport function firstOfflineExplainer({ device }: { device: DeviceNoun }): StatusCopy & {\n action: string;\n} {\n return {\n title: 'You can keep working offline',\n detail: `Documents you’ve opened recently keep working on ${device}. Your changes sync when you’re back online.`,\n action: 'Got it',\n };\n}\n\n/**\n * How a document's changes could be lost before they reach Athena:\n *\n * - `device`: saved on the device until they reach Athena (no connection, or a review the person\n * has not finished), but the device can remove them before then (a browser that hasn't granted\n * persistent storage, a private window);\n * - `held`: saved on the device and can't be sent (the person can only view the document now, or\n * Athena refused it), and the device can remove them;\n * - `memory`: kept only until the tab or app closes (the device keeps no copy).\n */\nexport type ChangesAtRisk = 'device' | 'held' | 'memory';\n\n/**\n * What an editor says above a document whose changes could be lost before they reach Athena, with\n * the copy it offers of them (Word's \"Upload pending\" and \"Changes not saved\"), by\n * {@link ChangesAtRisk}.\n */\nexport function changesAtRiskCopy({\n risk,\n device,\n}: {\n risk: ChangesAtRisk;\n device: DeviceNoun;\n}): StatusCopy & { download: string; downloadFailed: string } {\n const actions = {\n download: 'Download a copy',\n downloadFailed: 'Couldn’t download a copy. Try again.',\n };\n switch (risk) {\n case 'memory':\n return {\n title: 'Changes not saved',\n detail: `Your changes are kept only until you close ${openContainer(device)}. Download a copy to keep them.`,\n ...actions,\n };\n case 'held':\n return {\n title: 'Changes not saved',\n detail: `Your changes can’t be sent, and they’re saved only on ${device}, which can remove them. Download a copy to keep them.`,\n ...actions,\n };\n case 'device':\n return {\n title: 'Upload pending',\n detail: `Your changes are saved only on ${device} until they reach Athena, and ${device} can remove them before then. Download a copy to keep them.`,\n ...actions,\n };\n }\n}\n\n/**\n * The action that asks the device to keep what it holds until it syncs, where asking shows the\n * person a question of the device's own (Firefox asks whether the site may store data in\n * persistent storage): the app says why first (`changesAtRiskCopy`) and asks only on this press.\n */\nexport function keepOnDeviceAction({ device }: { device: DeviceNoun }): string {\n return `Keep them on ${device}`;\n}\n\n/** A browser whose own way of keeping a site's data longer the words point to. */\nexport type SaferStorageBrowser = 'safari-mac' | 'safari-ios';\n\n/**\n * Where changes made offline in Safari are kept longer: Safari keeps a site added to the Dock or\n * the Home Screen as an app of its own, which it is likelier to let keep its data, and the desktop\n * app keeps them in storage of its own.\n */\nexport function saferStorageHint({ browser }: { browser: SaferStorageBrowser }): string {\n return browser === 'safari-mac'\n ? 'To keep changes made offline safer in Safari, add Athena to the Dock (File ▸ Add to Dock), or use Athena Desktop.'\n : 'To keep changes made offline safer, add Athena to your Home Screen (Share ▸ Add to Home Screen).';\n}\n\n/**\n * What the app says when it finds that changes it kept on the device never reached Athena and the\n * device no longer has them (the browser removed them before they synced), naming how many\n * documents; each document's row says when its changes were made (`lostChangesItemCopy`).\n */\nexport function lostChangesCopy({\n device,\n documents,\n}: {\n device: DeviceNoun;\n documents: number;\n}): StatusCopy & { why: string; open: string; dismiss: string } {\n const which = documents === 1 ? 'a document' : plural(documents, 'document', 'documents');\n return {\n title: 'Changes not saved',\n detail: `Changes you made to ${which} on ${device} never reached Athena, and ${device} no longer has them.`,\n why: 'Browsers can remove what a site keeps when they run short of space, or when the site hasn’t been opened for a while.',\n open: 'Open',\n dismiss: 'Dismiss',\n };\n}\n\n/** One document's row under `lostChangesCopy`: the words before when its changes were made. */\nexport function lostChangesItemCopy({ device }: { device: DeviceNoun }): string {\n return `Changed on ${device}`;\n}\n\n/**\n * The summary heading of the one \"Waiting to sync\" list, from what waits on\n * the device; `null` when nothing does.\n */\nexport function waitingSummary({\n changes = 0,\n messages = 0,\n recordings = 0,\n}: {\n changes?: number;\n messages?: number;\n recordings?: number;\n}): string | null {\n const parts = [\n changes > 0 ? plural(changes, 'change', 'changes') : null,\n messages > 0 ? plural(messages, 'message', 'messages') : null,\n recordings > 0 ? plural(recordings, 'recording', 'recordings') : null,\n ].filter((part): part is string => part !== null);\n return parts.length === 0 ? null : `Waiting to sync: ${parts.join(', ')}`;\n}\n\n/**\n * Where one item in the \"Waiting to sync\" list stands: a change, a message or a\n * recording made while Athena couldn't be reached.\n */\nexport type WaitingItemState =\n /** Saved on the device; it syncs when Athena is back. */\n | 'waiting'\n /** Being sent to Athena now. */\n | 'syncing'\n /**\n * Kept only while this tab or app stays open: it couldn't be saved on the\n * device (the organization keeps nothing on devices, or storage failed).\n */\n | 'in-memory'\n /**\n * Saved on the device in something that isn't open (a document closed before\n * its edits synced, one made offline): it syncs when it is next opened with a\n * connection (opening it offline sends nothing).\n */\n | 'when-opened'\n /** Made in another workspace: it syncs once that workspace is open again, with a connection. */\n | 'other-workspace'\n /** It doesn't go on its own: the person chooses (a message that waited more than a day). */\n | 'needs-you'\n /** Athena didn't take it: the app adds why, and what the person can do about it. */\n | 'not-synced';\n\n/**\n * The state of one item in the \"Waiting to sync\" list, and the line that explains it. For an item\n * saved on the device (`waiting`, `when-opened`), `evictable` says the device may remove it before\n * it syncs (default `false`).\n */\nexport function waitingItemCopy({\n state,\n device,\n evictable = false,\n}: {\n state: WaitingItemState;\n device: DeviceNoun;\n evictable?: boolean;\n}): LabelCopy {\n const removable = evictable ? ` ${removableBeforeSync({ device, count: 1 })}` : '';\n switch (state) {\n case 'waiting':\n return {\n label: 'Waiting to sync',\n detail: `Saved on ${device}. It syncs when Athena is back.${removable}`,\n };\n case 'syncing':\n return { label: 'Syncing…', detail: `Sending what was saved on ${device}.` };\n case 'in-memory':\n return {\n label: 'Not saved yet',\n detail: `Kept only until you close ${openContainer(device)}. It syncs when Athena is back.`,\n };\n case 'when-opened':\n return {\n label: 'Not synced yet',\n detail: `Saved on ${device}. It syncs when you next open it with a connection.${removable}`,\n };\n case 'other-workspace':\n return {\n label: 'Waiting for its workspace',\n detail: 'It syncs once you’re in the workspace it was made in, with a connection.',\n };\n case 'needs-you':\n return { label: 'Waiting for you', detail: 'Open it to choose what happens to it.' };\n case 'not-synced':\n return { label: 'Couldn’t sync', detail: 'Athena didn’t take it.' };\n }\n}\n\n/** The headings of the \"Waiting to sync\" list, one per kind of item. */\nexport const WAITING_SECTION_TITLES = {\n changes: 'Changes',\n messages: 'Messages',\n recordings: 'Recordings',\n} as const;\n\n/**\n * What the person can do with an item Athena didn't take (`not-synced`) in the\n * \"Waiting to sync\" list: send it again, or delete the copy on the device. That\n * copy is the item's only one, so the app asks first (`deleteFromDeviceConfirm`)\n * and never deletes it as a side effect of anything else.\n */\nexport function notSyncedActions({ device }: { device: DeviceNoun }): {\n retry: string;\n remove: string;\n} {\n return { retry: 'Try again', remove: `Delete from ${device}` };\n}\n\n/** One of `notSyncedActions`, or showing where the item is kept on the device. */\nexport type NotSyncedAction = 'retry' | 'reveal' | 'remove';\n\n/**\n * What the person hears when the device couldn't do what they asked for an\n * item Athena didn't take (its files, not Athena, failed).\n */\nexport function notSyncedActionFailed({\n action,\n device,\n}: {\n action: NotSyncedAction;\n device: DeviceNoun;\n}): string {\n switch (action) {\n case 'retry':\n return 'Couldn’t send it again. Try again in a moment.';\n case 'reveal':\n return `Couldn’t show where it’s kept on ${device}.`;\n case 'remove':\n return `Couldn’t delete it from ${device}.`;\n }\n}\n\n/**\n * The question before deleting such an item's copy on the device, naming the\n * item: the destructive choice is never the default.\n */\nexport function deleteFromDeviceConfirm({\n name,\n device,\n}: {\n name: string;\n device: DeviceNoun;\n}): StatusCopy & { confirm: string; cancel: string } {\n return {\n title: `Delete “${name}” from ${device}?`,\n detail: 'Athena didn’t take it, so this is its only copy. Deleting it can’t be undone.',\n confirm: 'Delete',\n cancel: 'Cancel',\n };\n}\n"],"names":[],"mappings":"AA6TO,MAAM,6BAA6B;AAgI1C,MAAM,kBAAkB;AAMjB,MAAM,mBAA8B;AAAA,EACzC,OAAO;AAAA,EACP,QAAQ,sBAAsB,eAAe;AAC/C;AAaO,SAAS,0BAA0B;AAAA,EACxC;AAAA,EACA;AACF,GAGW;AACT,QAAM,YAAY,aAA4C,QAAQ,kBAAkB,eAAe;AACvG,SAAO,eACH,iEAAiE,SAAS,KAC1E;AACN;AAGA,SAAS,WAAW,WAA4B;AAC9C,SAAO,YAAY,6BAA6B;AAClD;AAQO,SAAS,+BAA+B;AAAA,EAC7C,YAAY;AACd,IAEI,IAAY;AACd,SAAO,sDAAsD,WAAW,SAAS,CAAC,kBAChF,YAAY,OAAO,gBACrB;AACF;AAOO,SAAS,6BAA6B;AAAA,EAC3C,YAAY;AACd,IAEI,IAAY;AACd,SAAO,gFAAgF;AAAA,IACrF;AAAA,EAAA,CACD;AACH;"}
|
|
@@ -12,6 +12,7 @@ export { type ConversationListPage, type ConversationSummary, mergeConversationI
|
|
|
12
12
|
export { isLegacyThreadReadOnlyRejection, isStatewireChannelTeardown, isStatewireRewindAnchorRejection, statewireRejectionDetailMessage, statewireRejectionPayload, } from "./errors";
|
|
13
13
|
export { asHitlApproval, findAbandonedInterrupt, HITL_PENDING_RESUME_GRACE_MS, type HitlApprovalPayload, hitlResumeApprove, hitlResumeReject, interruptMirrorPayload, interruptResumeDecline, isApprovalCardInterrupt, pendingActionRequests, readAbandonedInputRequests, readInterruptMessage, readInterruptMirror, readQueuedLaneDecisions, toolCallIdsAwaitingApproval, useApprovalResumeLock, } from "./interrupts";
|
|
14
14
|
export { type DeepAgentMessageWindowDeclaration } from "./message-window";
|
|
15
|
+
export { clearDeepAgentHistoryStores, DEEP_AGENT_TURN_STEPS_KEY, type DeepAgentHistorySend, type DeepAgentTurnStepsMarker, deepAgentHistoryStore, loadDeepAgentTurnSteps, peekDeepAgentHistoryStore, readTurnStepsMarker, } from "./history-store";
|
|
15
16
|
export { ATHENA_RUN_MARKER_KEY, isQueuedLaneMessage, } from "./messages";
|
|
16
17
|
export { isDeepAgentHostState } from "./protocol";
|
|
17
18
|
export { parkIncompatibleSessionCommands } from "./quota-safe-local-storage-session";
|
|
@@ -75,19 +75,19 @@ export declare const threadHandoffFlowMachine: import("xstate").StateMachine<Han
|
|
|
75
75
|
} | {
|
|
76
76
|
kind: "office_invitation";
|
|
77
77
|
host: "excel" | "powerpoint" | "word";
|
|
78
|
-
} | {
|
|
79
|
-
kind: "chrome";
|
|
80
78
|
} | {
|
|
81
79
|
kind: "spaces";
|
|
82
80
|
url: string;
|
|
83
|
-
} | {
|
|
84
|
-
kind: "next";
|
|
85
81
|
} | {
|
|
86
82
|
kind: "desktop";
|
|
87
83
|
scheme_url: string;
|
|
88
84
|
download_url: string;
|
|
89
85
|
deployment_url: string;
|
|
90
86
|
protocol_version: 1;
|
|
87
|
+
} | {
|
|
88
|
+
kind: "chrome";
|
|
89
|
+
} | {
|
|
90
|
+
kind: "next";
|
|
91
91
|
};
|
|
92
92
|
}, {
|
|
93
93
|
run: ThreadHandoffRun;
|
|
@@ -138,19 +138,19 @@ export declare const threadHandoffFlowMachine: import("xstate").StateMachine<Han
|
|
|
138
138
|
} | {
|
|
139
139
|
kind: "office_invitation";
|
|
140
140
|
host: "excel" | "powerpoint" | "word";
|
|
141
|
-
} | {
|
|
142
|
-
kind: "chrome";
|
|
143
141
|
} | {
|
|
144
142
|
kind: "spaces";
|
|
145
143
|
url: string;
|
|
146
|
-
} | {
|
|
147
|
-
kind: "next";
|
|
148
144
|
} | {
|
|
149
145
|
kind: "desktop";
|
|
150
146
|
scheme_url: string;
|
|
151
147
|
download_url: string;
|
|
152
148
|
deployment_url: string;
|
|
153
149
|
protocol_version: 1;
|
|
150
|
+
} | {
|
|
151
|
+
kind: "chrome";
|
|
152
|
+
} | {
|
|
153
|
+
kind: "next";
|
|
154
154
|
};
|
|
155
155
|
}, {
|
|
156
156
|
run: ThreadHandoffRun;
|
|
@@ -1011,6 +1011,14 @@ const TOOL_DISPLAY_META = {
|
|
|
1011
1011
|
icon: "scan-search",
|
|
1012
1012
|
emoji: "🧐"
|
|
1013
1013
|
},
|
|
1014
|
+
read_asset_author: {
|
|
1015
|
+
displayName: "Who Wrote This",
|
|
1016
|
+
activeLabel: "Reading who wrote it",
|
|
1017
|
+
completedLabel: "Read who wrote it",
|
|
1018
|
+
category: "Spaces",
|
|
1019
|
+
icon: "pen-line",
|
|
1020
|
+
emoji: "✍️"
|
|
1021
|
+
},
|
|
1014
1022
|
history_suggestions: {
|
|
1015
1023
|
displayName: "Suggestions",
|
|
1016
1024
|
activeLabel: "Reading waiting suggestions",
|
|
@@ -1161,6 +1169,14 @@ const TOOL_DISPLAY_META = {
|
|
|
1161
1169
|
icon: "camera",
|
|
1162
1170
|
emoji: "📸"
|
|
1163
1171
|
},
|
|
1172
|
+
restore_spreadsheet_sheets: {
|
|
1173
|
+
displayName: "Restore Sheets",
|
|
1174
|
+
activeLabel: "Restoring sheets",
|
|
1175
|
+
completedLabel: "Sheets restored",
|
|
1176
|
+
category: "Spreadsheet",
|
|
1177
|
+
icon: "table",
|
|
1178
|
+
emoji: "↩️"
|
|
1179
|
+
},
|
|
1164
1180
|
create_table: {
|
|
1165
1181
|
displayName: "Create Table",
|
|
1166
1182
|
activeLabel: "Creating table",
|