@athenaintel/react 0.53.1 → 0.54.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.
Files changed (135) hide show
  1. package/README.md +18 -1
  2. package/dist/agent-runtime/src/call-recap.cjs +2 -3
  3. package/dist/agent-runtime/src/call-recap.cjs.map +1 -1
  4. package/dist/agent-runtime/src/call-recap.d.ts +1 -2
  5. package/dist/agent-runtime/src/call-recap.js +1 -2
  6. package/dist/agent-runtime/src/call-recap.js.map +1 -1
  7. package/dist/agent-runtime/src/deep-agent-transport-recipe.cjs +7 -3
  8. package/dist/agent-runtime/src/deep-agent-transport-recipe.cjs.map +1 -1
  9. package/dist/agent-runtime/src/deep-agent-transport-recipe.d.ts +10 -2
  10. package/dist/agent-runtime/src/deep-agent-transport-recipe.js +8 -4
  11. package/dist/agent-runtime/src/deep-agent-transport-recipe.js.map +1 -1
  12. package/dist/agent-runtime/src/dictation-session.d.ts +158 -0
  13. package/dist/agent-runtime/src/edit-command.cjs +6 -0
  14. package/dist/agent-runtime/src/edit-command.cjs.map +1 -1
  15. package/dist/agent-runtime/src/edit-command.d.ts +8 -0
  16. package/dist/agent-runtime/src/edit-command.js +6 -0
  17. package/dist/agent-runtime/src/edit-command.js.map +1 -1
  18. package/dist/agent-runtime/src/generated/runconfig-contract.cjs +6 -0
  19. package/dist/agent-runtime/src/generated/runconfig-contract.cjs.map +1 -1
  20. package/dist/agent-runtime/src/generated/runconfig-contract.d.ts +13 -0
  21. package/dist/agent-runtime/src/generated/runconfig-contract.js +6 -0
  22. package/dist/agent-runtime/src/generated/runconfig-contract.js.map +1 -1
  23. package/dist/agent-runtime/src/index.d.ts +6 -6
  24. package/dist/agent-runtime/src/message-window.cjs +111 -7
  25. package/dist/agent-runtime/src/message-window.cjs.map +1 -1
  26. package/dist/agent-runtime/src/message-window.d.ts +66 -6
  27. package/dist/agent-runtime/src/message-window.js +112 -8
  28. package/dist/agent-runtime/src/message-window.js.map +1 -1
  29. package/dist/agent-runtime/src/offline-status.cjs.map +1 -1
  30. package/dist/agent-runtime/src/offline-status.d.ts +33 -0
  31. package/dist/agent-runtime/src/offline-status.js.map +1 -1
  32. package/dist/agent-runtime/src/runtime.d.ts +4 -3
  33. package/dist/agent-runtime/src/subagent-transcripts.cjs +23 -1
  34. package/dist/agent-runtime/src/subagent-transcripts.cjs.map +1 -1
  35. package/dist/agent-runtime/src/subagent-transcripts.js +25 -3
  36. package/dist/agent-runtime/src/subagent-transcripts.js.map +1 -1
  37. package/dist/agent-runtime/src/subagents.cjs +112 -0
  38. package/dist/agent-runtime/src/subagents.cjs.map +1 -1
  39. package/dist/agent-runtime/src/subagents.d.ts +103 -0
  40. package/dist/agent-runtime/src/subagents.js +113 -1
  41. package/dist/agent-runtime/src/subagents.js.map +1 -1
  42. package/dist/agent-runtime/src/thread-handoff-protocol.d.ts +37 -9
  43. package/dist/agent-runtime/src/thread-handoff.d.ts +1 -1
  44. package/dist/agent-runtime/src/tool-metadata.cjs +8 -0
  45. package/dist/agent-runtime/src/tool-metadata.cjs.map +1 -1
  46. package/dist/agent-runtime/src/tool-metadata.js +8 -0
  47. package/dist/agent-runtime/src/tool-metadata.js.map +1 -1
  48. package/dist/agent-runtime/src/use-deep-agent-thread.cjs +6 -0
  49. package/dist/agent-runtime/src/use-deep-agent-thread.cjs.map +1 -1
  50. package/dist/agent-runtime/src/use-deep-agent-thread.js +7 -1
  51. package/dist/agent-runtime/src/use-deep-agent-thread.js.map +1 -1
  52. package/dist/agent-runtime/src/use-deep-agent-window-sync.cjs +39 -10
  53. package/dist/agent-runtime/src/use-deep-agent-window-sync.cjs.map +1 -1
  54. package/dist/agent-runtime/src/use-deep-agent-window-sync.d.ts +14 -4
  55. package/dist/agent-runtime/src/use-deep-agent-window-sync.js +41 -12
  56. package/dist/agent-runtime/src/use-deep-agent-window-sync.js.map +1 -1
  57. package/dist/agent-runtime/src/use-thread-handoff-flow.d.ts +5 -9
  58. package/dist/agent-runtime/src/voice-presentation.cjs +34 -0
  59. package/dist/agent-runtime/src/voice-presentation.cjs.map +1 -1
  60. package/dist/agent-runtime/src/voice-presentation.d.ts +35 -0
  61. package/dist/agent-runtime/src/voice-presentation.js +34 -0
  62. package/dist/agent-runtime/src/voice-presentation.js.map +1 -1
  63. package/dist/assets/AthenaAssetEmbed.cjs +4 -2
  64. package/dist/assets/AthenaAssetEmbed.cjs.map +1 -1
  65. package/dist/assets/AthenaAssetEmbed.d.ts +17 -8
  66. package/dist/assets/AthenaAssetEmbed.js +4 -2
  67. package/dist/assets/AthenaAssetEmbed.js.map +1 -1
  68. package/dist/assets/use-asset-embed.cjs.map +1 -1
  69. package/dist/assets/use-asset-embed.d.ts +7 -5
  70. package/dist/assets/use-asset-embed.js.map +1 -1
  71. package/dist/auth/account-identity.cjs +69 -0
  72. package/dist/auth/account-identity.cjs.map +1 -0
  73. package/dist/auth/account-identity.d.ts +57 -0
  74. package/dist/auth/account-identity.js +69 -0
  75. package/dist/auth/account-identity.js.map +1 -0
  76. package/dist/chat/AthenaChat.cjs +1 -1
  77. package/dist/chat/AthenaChat.cjs.map +1 -1
  78. package/dist/chat/AthenaChat.js +1 -1
  79. package/dist/chat/AthenaChat.js.map +1 -1
  80. package/dist/chat/super-grouping/toolkit-map.cjs +1 -0
  81. package/dist/chat/super-grouping/toolkit-map.cjs.map +1 -1
  82. package/dist/chat/super-grouping/toolkit-map.js +1 -0
  83. package/dist/chat/super-grouping/toolkit-map.js.map +1 -1
  84. package/dist/chat/use-statewire-approval-request.cjs +5 -5
  85. package/dist/chat/use-statewire-approval-request.cjs.map +1 -1
  86. package/dist/chat/use-statewire-approval-request.d.ts +7 -0
  87. package/dist/chat/use-statewire-approval-request.js +5 -5
  88. package/dist/chat/use-statewire-approval-request.js.map +1 -1
  89. package/dist/chat-ui/mentions/use-account-workspace-id.cjs +18 -58
  90. package/dist/chat-ui/mentions/use-account-workspace-id.cjs.map +1 -1
  91. package/dist/chat-ui/mentions/use-account-workspace-id.js +18 -58
  92. package/dist/chat-ui/mentions/use-account-workspace-id.js.map +1 -1
  93. package/dist/chat-ui/messages/voice-delegation-card.cjs +93 -25
  94. package/dist/chat-ui/messages/voice-delegation-card.cjs.map +1 -1
  95. package/dist/chat-ui/messages/voice-delegation-card.d.ts +16 -7
  96. package/dist/chat-ui/messages/voice-delegation-card.js +95 -27
  97. package/dist/chat-ui/messages/voice-delegation-card.js.map +1 -1
  98. package/dist/chat-ui/thread/handoff-cursor.cjs +2 -2
  99. package/dist/chat-ui/thread/handoff-cursor.cjs.map +1 -1
  100. package/dist/chat-ui/thread/handoff-cursor.js +3 -3
  101. package/dist/chat-ui/thread/handoff-cursor.js.map +1 -1
  102. package/dist/collab/client.cjs +94 -6
  103. package/dist/collab/client.cjs.map +1 -1
  104. package/dist/collab/client.d.ts +27 -0
  105. package/dist/collab/client.js +94 -6
  106. package/dist/collab/client.js.map +1 -1
  107. package/dist/collab/index.d.ts +1 -1
  108. package/dist/keryx-http/src/initial-sync.cjs +121 -0
  109. package/dist/keryx-http/src/initial-sync.cjs.map +1 -0
  110. package/dist/keryx-http/src/initial-sync.d.ts +77 -0
  111. package/dist/keryx-http/src/initial-sync.js +104 -0
  112. package/dist/keryx-http/src/initial-sync.js.map +1 -0
  113. package/dist/keryx-http/src/unconfirmed-update-attributions.cjs +8 -0
  114. package/dist/keryx-http/src/unconfirmed-update-attributions.cjs.map +1 -1
  115. package/dist/keryx-http/src/unconfirmed-update-attributions.d.ts +6 -0
  116. package/dist/keryx-http/src/unconfirmed-update-attributions.js +8 -0
  117. package/dist/keryx-http/src/unconfirmed-update-attributions.js.map +1 -1
  118. package/dist/react/package.json.cjs +1 -1
  119. package/dist/react/package.json.js +1 -1
  120. package/dist/styles.css +1 -1
  121. package/dist/threads/api.cjs +1 -2
  122. package/dist/threads/api.cjs.map +1 -1
  123. package/dist/threads/api.d.ts +3 -2
  124. package/dist/threads/api.js +1 -2
  125. package/dist/threads/api.js.map +1 -1
  126. package/dist/threads/statewire-thread-list.cjs +5 -7
  127. package/dist/threads/statewire-thread-list.cjs.map +1 -1
  128. package/dist/threads/statewire-thread-list.js +7 -9
  129. package/dist/threads/statewire-thread-list.js.map +1 -1
  130. package/dist/tools/subagent-task-tool-ui.cjs +15 -5
  131. package/dist/tools/subagent-task-tool-ui.cjs.map +1 -1
  132. package/dist/tools/subagent-task-tool-ui.d.ts +5 -4
  133. package/dist/tools/subagent-task-tool-ui.js +16 -6
  134. package/dist/tools/subagent-task-tool-ui.js.map +1 -1
  135. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"use-account-workspace-id.js","sources":["../../../src/chat-ui/mentions/use-account-workspace-id.ts"],"sourcesContent":["import { useQuery } from '@tanstack/react-query';\nimport { useAthenaCredentialIdentityEpoch } from '../../auth/AthenaCredentialSourceContext';\nimport { jwtScope, jwtSubject } from '../../auth/credential-source';\nimport { useCatalogQueryClient } from '../../catalog/runtime-catalog';\nimport { keyPartFingerprint } from '../../lib/use-athena-rest-query';\nimport { withCredentialRenewal } from '../../runtime/credential-renewal';\nimport type { AthenaStatewireTokenGetter } from '../../runtime/statewire-auth-headers';\nimport { type AccountIdentity, fetchAccountIdentity } from '../../threads/api';\nimport type { MentionAuthContext } from './suggestions/data/api-client';\n\n/** The credential's agora identity, as far as it is known yet. */\nexport interface AccountIdentityState {\n /** Agora's user id (`user_…`) for the credential; unknown until `/me` answers. */\n userId: string | undefined;\n /** The host's workspace when it names one, else the credential's current workspace. */\n workspaceId: string | undefined;\n}\n\nexport interface AccountIdentityOptions {\n /**\n * The provider's token getter, which `auth.getAccessToken` wraps (`AthenaChat`\n * passes `useAthenaConfig().getToken`). Its identity keeps two providers\n * apart when only their getters know who they sign in.\n */\n tokenGetter?: AthenaStatewireTokenGetter;\n}\n\n/** One id per token getter, so the key can name a getter without holding it. */\nconst tokenGetterIds = new WeakMap<AthenaStatewireTokenGetter, number>();\nlet lastTokenGetterId = 0;\n\nfunction tokenGetterId(getToken: AthenaStatewireTokenGetter | undefined): string {\n if (getToken === undefined) return '';\n let id = tokenGetterIds.get(getToken);\n if (id === undefined) {\n lastTokenGetterId += 1;\n id = lastTokenGetterId;\n tokenGetterIds.set(getToken, id);\n }\n return `getter:${id}`;\n}\n\n/**\n * Who a held token acts as, and where, without the token: its JWT subject and\n * the claims scoping it to a workspace or org (`jwtScope`), or the token\n * itself when it is opaque, fingerprinted. A renewal for the same person and\n * scope keeps it, so a rotation is not a new identity; the same person's token\n * scoped to another workspace is.\n */\nfunction heldTokenIdentity(token: string | null | undefined): string {\n if (!token) return '';\n const subject = jwtSubject(token);\n if (subject === null) return `token:${keyPartFingerprint(token)}`;\n const scope = jwtScope(token);\n return scope === null\n ? `sub:${keyPartFingerprint(subject)}`\n : `sub:${keyPartFingerprint(subject)}|scope:${keyPartFingerprint(scope)}`;\n}\n\n/**\n * One `/me` read per credential, through the host's query cache (never a\n * process-wide user-id map), keyed by the credential the read is sent with and\n * never by the secret: the backend, the signed-in person, the API key's\n * fingerprint, the provider's token getter, who the held token acts as and\n * which workspace it is scoped to, and the provider's credential identity\n * epoch (`useAthenaCredentialIdentityEpoch`), which moves once the getter has\n * handed out a token that acts as someone else. A renewal for the same person\n * and scope keeps the key, so the answer stays put; an API-key switch, a held\n * token scoped to another workspace, or a getter now signing in someone else,\n * reads `/me` again under a new key, never a cached answer for the previous\n * credential. The epoch follows the person, not the scope: a host getter that\n * alone hands out the same person's token for another workspace is read again\n * once the answer goes stale. `enabled` is the caller's: a reader that only\n * needs the workspace skips the read when the host names one.\n */\nfunction useAccountIdentityRead(\n auth: MentionAuthContext | null,\n { enabled, tokenGetter }: { enabled: boolean } & AccountIdentityOptions\n): AccountIdentity | null | undefined {\n const queryClient = useCatalogQueryClient();\n const identityEpoch = useAthenaCredentialIdentityEpoch();\n const query = useQuery(\n {\n queryKey: [\n 'athena-sdk',\n 'account',\n auth?.backendUrl ?? '',\n auth?.userId ?? '',\n auth?.apiKey ? keyPartFingerprint(auth.apiKey) : '',\n tokenGetterId(tokenGetter),\n heldTokenIdentity(auth?.accessToken),\n identityEpoch,\n ],\n queryFn: async ({ signal }) => {\n if (!auth) return null;\n return withCredentialRenewal(\n async ({ force }: { force?: boolean } = {}) => ({\n token:\n (force ? await auth.renewAccessToken?.() : await auth.getAccessToken?.()) ??\n auth.accessToken ??\n null,\n apiKey: auth.apiKey,\n credentials: auth.credentials,\n }),\n (resolved) => fetchAccountIdentity(auth.backendUrl, resolved, { signal })\n );\n },\n enabled: auth !== null && enabled,\n retry: false,\n staleTime: 60_000,\n },\n queryClient\n );\n return query.data;\n}\n\n/** The workspace mentions search: the host's when it names one, else the credential's own. */\nexport function useAccountWorkspaceId(auth: MentionAuthContext | null): string | undefined {\n const identity = useAccountIdentityRead(auth, { enabled: !auth?.workspaceId });\n return auth?.workspaceId ?? identity?.workspaceId ?? undefined;\n}\n\n/**\n * The credential's agora user id and workspace from the same read. The auth\n * context carries the identity provider's user id, not agora's, so \"who is\n * the viewer\" (an answer label's \"for you\") needs this one, and it reads\n * `/me` even when the host names a workspace.\n */\nexport function useAccountIdentity(\n auth: MentionAuthContext | null,\n options: AccountIdentityOptions = {}\n): AccountIdentityState {\n const identity = useAccountIdentityRead(auth, { enabled: true, ...options });\n return {\n userId: identity?.userId,\n workspaceId: auth?.workspaceId ?? identity?.workspaceId ?? undefined,\n };\n}\n"],"names":[],"mappings":";;;;;;;AA4BA,MAAM,qCAAqB,QAAA;AAC3B,IAAI,oBAAoB;AAExB,SAAS,cAAc,UAA0D;AAC/E,MAAI,aAAa,OAAW,QAAO;AACnC,MAAI,KAAK,eAAe,IAAI,QAAQ;AACpC,MAAI,OAAO,QAAW;AACpB,yBAAqB;AACrB,SAAK;AACL,mBAAe,IAAI,UAAU,EAAE;AAAA,EACjC;AACA,SAAO,UAAU,EAAE;AACrB;AASA,SAAS,kBAAkB,OAA0C;AACnE,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,UAAU,WAAW,KAAK;AAChC,MAAI,YAAY,KAAM,QAAO,SAAS,mBAAmB,KAAK,CAAC;AAC/D,QAAM,QAAQ,SAAS,KAAK;AAC5B,SAAO,UAAU,OACb,OAAO,mBAAmB,OAAO,CAAC,KAClC,OAAO,mBAAmB,OAAO,CAAC,UAAU,mBAAmB,KAAK,CAAC;AAC3E;AAkBA,SAAS,uBACP,MACA,EAAE,SAAS,eACyB;AACpC,QAAM,cAAc,sBAAA;AACpB,QAAM,gBAAgB,iCAAA;AACtB,QAAM,QAAQ;AAAA,IACZ;AAAA,MACE,UAAU;AAAA,QACR;AAAA,QACA;AAAA,SACA,6BAAM,eAAc;AAAA,SACpB,6BAAM,WAAU;AAAA,SAChB,6BAAM,UAAS,mBAAmB,KAAK,MAAM,IAAI;AAAA,QACjD,cAAc,WAAW;AAAA,QACzB,kBAAkB,6BAAM,WAAW;AAAA,QACnC;AAAA,MAAA;AAAA,MAEF,SAAS,OAAO,EAAE,aAAa;AAC7B,YAAI,CAAC,KAAM,QAAO;AAClB,eAAO;AAAA,UACL,OAAO,EAAE,MAAA,IAA+B,OAAC;;AAAO;AAAA,cAC9C,QACG,QAAQ,QAAM,UAAK,qBAAL,iCAA4B,QAAM,UAAK,mBAAL,mCACjD,KAAK,eACL;AAAA,cACF,QAAQ,KAAK;AAAA,cACb,aAAa,KAAK;AAAA,YAAA;AAAA;AAAA,UAEpB,CAAC,aAAa,qBAAqB,KAAK,YAAY,UAAU,EAAE,QAAQ;AAAA,QAAA;AAAA,MAE5E;AAAA,MACA,SAAS,SAAS,QAAQ;AAAA,MAC1B,OAAO;AAAA,MACP,WAAW;AAAA,IAAA;AAAA,IAEb;AAAA,EAAA;AAEF,SAAO,MAAM;AACf;AAcO,SAAS,mBACd,MACA,UAAkC,IACZ;AACtB,QAAM,WAAW,uBAAuB,MAAM,EAAE,SAAS,MAAM,GAAG,SAAS;AAC3E,SAAO;AAAA,IACL,QAAQ,qCAAU;AAAA,IAClB,cAAa,6BAAM,iBAAe,qCAAU,gBAAe;AAAA,EAAA;AAE/D;"}
1
+ {"version":3,"file":"use-account-workspace-id.js","sources":["../../../src/chat-ui/mentions/use-account-workspace-id.ts"],"sourcesContent":["import { useAccountIdentityQuery } from '../../auth/account-identity';\nimport { useCatalogQueryClient } from '../../catalog/runtime-catalog';\nimport type { AthenaStatewireTokenGetter } from '../../runtime/statewire-auth-headers';\nimport type { AccountIdentity } from '../../threads/api';\nimport type { MentionAuthContext } from './suggestions/data/api-client';\n\n/** The credential's agora identity, as far as it is known yet. */\nexport interface AccountIdentityState {\n /** Agora's user id (`user_…`) for the credential; unknown until `/me` answers. */\n userId: string | undefined;\n /** The host's workspace when it names one, else the credential's current workspace. */\n workspaceId: string | undefined;\n}\n\nexport interface AccountIdentityOptions {\n /**\n * The provider's token getter, which `auth.getAccessToken` wraps (`AthenaChat`\n * passes `useAthenaConfig().getToken`). Its identity keeps two providers\n * apart when only their getters know who they sign in.\n */\n tokenGetter?: AthenaStatewireTokenGetter;\n}\n\n/**\n * The chat's read of the credential's identity (`useAccountIdentityQuery`),\n * through the host's query cache: inside a provider with a session list, the\n * list's own read under the same key, so the page sends one `/me`. `enabled`\n * is the caller's: a reader that only needs the workspace skips the read when\n * the host names one.\n */\nfunction useAccountIdentityRead(\n auth: MentionAuthContext | null,\n { enabled, tokenGetter }: { enabled: boolean } & AccountIdentityOptions\n): AccountIdentity | undefined {\n const query = useAccountIdentityQuery({\n credential: {\n backendUrl: auth?.backendUrl ?? '',\n principal: auth?.userId,\n apiKey: auth?.apiKey,\n heldToken: auth?.accessToken,\n tokenGetter,\n },\n resolveAuth: async ({ force }: { force?: boolean } = {}) => ({\n token:\n (force ? await auth?.renewAccessToken?.() : await auth?.getAccessToken?.()) ??\n auth?.accessToken ??\n null,\n apiKey: auth?.apiKey,\n }),\n enabled: auth !== null && enabled,\n queryClient: useCatalogQueryClient(),\n });\n return query.data;\n}\n\n/** The workspace mentions search: the host's when it names one, else the credential's own. */\nexport function useAccountWorkspaceId(auth: MentionAuthContext | null): string | undefined {\n const identity = useAccountIdentityRead(auth, { enabled: !auth?.workspaceId });\n return auth?.workspaceId ?? identity?.workspaceId ?? undefined;\n}\n\n/**\n * The credential's agora user id and workspace from the same read. The auth\n * context carries the identity provider's user id, not agora's, so \"who is\n * the viewer\" (an answer label's \"for you\") needs this one, and it reads\n * `/me` even when the host names a workspace.\n */\nexport function useAccountIdentity(\n auth: MentionAuthContext | null,\n options: AccountIdentityOptions = {}\n): AccountIdentityState {\n const identity = useAccountIdentityRead(auth, { enabled: true, ...options });\n return {\n userId: identity?.userId,\n workspaceId: auth?.workspaceId ?? identity?.workspaceId ?? undefined,\n };\n}\n"],"names":[],"mappings":";;AA8BA,SAAS,uBACP,MACA,EAAE,SAAS,eACkB;AAC7B,QAAM,QAAQ,wBAAwB;AAAA,IACpC,YAAY;AAAA,MACV,aAAY,6BAAM,eAAc;AAAA,MAChC,WAAW,6BAAM;AAAA,MACjB,QAAQ,6BAAM;AAAA,MACd,WAAW,6BAAM;AAAA,MACjB;AAAA,IAAA;AAAA,IAEF,aAAa,OAAO,EAAE,MAAA,IAA+B,OAAC;;AAAO;AAAA,QAC3D,QACG,QAAQ,QAAM,kCAAM,qBAAN,iCAA6B,QAAM,kCAAM,mBAAN,oCAClD,6BAAM,gBACN;AAAA,QACF,QAAQ,6BAAM;AAAA,MAAA;AAAA;AAAA,IAEhB,SAAS,SAAS,QAAQ;AAAA,IAC1B,aAAa,sBAAA;AAAA,EAAsB,CACpC;AACD,SAAO,MAAM;AACf;AAcO,SAAS,mBACd,MACA,UAAkC,IACZ;AACtB,QAAM,WAAW,uBAAuB,MAAM,EAAE,SAAS,MAAM,GAAG,SAAS;AAC3E,SAAO;AAAA,IACL,QAAQ,qCAAU;AAAA,IAClB,cAAa,6BAAM,iBAAe,qCAAU,gBAAe;AAAA,EAAA;AAE/D;"}
@@ -3,33 +3,87 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const jsxRuntime = require("react/jsx-runtime");
4
4
  const react = require("@assistant-ui/react");
5
5
  const lucideReact = require("lucide-react");
6
+ const require$$0 = require("react");
6
7
  const useAthenaRunLive = require("../../chat/use-athena-run-live.cjs");
8
+ const voicePresentation = require("../../agent-runtime/src/voice-presentation.cjs");
7
9
  const cnfast = require("cnfast");
8
- const VoiceDelegationCard = ({ text, live, className }) => /* @__PURE__ */ jsxRuntime.jsx(
9
- "div",
10
- {
11
- className: cnfast.cn("flex justify-end px-1 pb-0.5", className),
12
- "data-testid": "voice-delegation-card",
13
- children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "max-w-[85%] rounded-xl border border-primary/20 bg-primary/5 px-3 py-2", children: [
14
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-1 flex items-center gap-1.5", children: [
15
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-flex size-4 items-center justify-center rounded bg-primary/15", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AudioLinesIcon, { className: "size-2.5 text-primary", "aria-hidden": true }) }),
16
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] font-medium uppercase tracking-wide text-primary", children: "Voice task" }),
17
- live ? /* @__PURE__ */ jsxRuntime.jsxs(
18
- "span",
19
- {
20
- className: "inline-flex items-center gap-1 text-[10px] text-primary/80",
21
- "data-testid": "voice-delegation-live",
22
- children: [
23
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.LoaderIcon, { className: "size-2.5 animate-spin", "aria-hidden": true }),
24
- "running"
25
- ]
26
- }
27
- ) : null
28
- ] }),
29
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "whitespace-pre-wrap break-words text-sm text-foreground", children: text })
30
- ] })
10
+ const VoiceDelegationHeading = ({ live }) => /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
11
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-flex size-4 items-center justify-center rounded bg-primary/15", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AudioLinesIcon, { className: "size-2.5 text-primary", "aria-hidden": true }) }),
12
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] font-medium uppercase tracking-wide text-primary", children: "Voice task" }),
13
+ live ? /* @__PURE__ */ jsxRuntime.jsxs(
14
+ "span",
15
+ {
16
+ className: "inline-flex items-center gap-1 text-[10px] text-primary/80",
17
+ "data-testid": "voice-delegation-live",
18
+ children: [
19
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.LoaderIcon, { className: "size-2.5 animate-spin", "aria-hidden": true }),
20
+ "running"
21
+ ]
22
+ }
23
+ ) : null
24
+ ] });
25
+ const VoiceDelegationCard = ({
26
+ text,
27
+ live,
28
+ echo = false,
29
+ className
30
+ }) => {
31
+ const [open, setOpen] = require$$0.useState(false);
32
+ const textId = require$$0.useId();
33
+ if (echo) {
34
+ return /* @__PURE__ */ jsxRuntime.jsxs(
35
+ "div",
36
+ {
37
+ className: cnfast.cn("flex flex-col items-end px-1 pb-0.5", className),
38
+ "data-testid": "voice-delegation-card",
39
+ "data-echo": "true",
40
+ children: [
41
+ /* @__PURE__ */ jsxRuntime.jsxs(
42
+ "button",
43
+ {
44
+ type: "button",
45
+ className: "inline-flex items-center gap-1.5 rounded-full border border-primary/20 bg-primary/5 px-2 py-0.5 hover:bg-primary/10",
46
+ "aria-expanded": open,
47
+ "aria-controls": textId,
48
+ onClick: () => setOpen((value) => !value),
49
+ "data-testid": "voice-delegation-toggle",
50
+ children: [
51
+ /* @__PURE__ */ jsxRuntime.jsx(VoiceDelegationHeading, { live }),
52
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "sr-only", children: open ? "Hide the task" : "Show the task" }),
53
+ /* @__PURE__ */ jsxRuntime.jsx(
54
+ lucideReact.ChevronDownIcon,
55
+ {
56
+ className: cnfast.cn("size-3 text-primary/70 transition-transform", open && "rotate-180"),
57
+ "aria-hidden": true
58
+ }
59
+ )
60
+ ]
61
+ }
62
+ ),
63
+ open ? /* @__PURE__ */ jsxRuntime.jsx(
64
+ "div",
65
+ {
66
+ id: textId,
67
+ className: "mt-1 max-w-[85%] whitespace-pre-wrap break-words rounded-xl border border-primary/20 bg-primary/5 px-3 py-2 text-sm text-foreground",
68
+ children: text
69
+ }
70
+ ) : null
71
+ ]
72
+ }
73
+ );
31
74
  }
32
- );
75
+ return /* @__PURE__ */ jsxRuntime.jsx(
76
+ "div",
77
+ {
78
+ className: cnfast.cn("flex justify-end px-1 pb-0.5", className),
79
+ "data-testid": "voice-delegation-card",
80
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "max-w-[85%] rounded-xl border border-primary/20 bg-primary/5 px-3 py-2", children: [
81
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-1 flex items-center gap-1.5", children: /* @__PURE__ */ jsxRuntime.jsx(VoiceDelegationHeading, { live }) }),
82
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "whitespace-pre-wrap break-words text-sm text-foreground", children: text })
83
+ ] })
84
+ }
85
+ );
86
+ };
33
87
  const VoiceDelegationMessage = ({ className }) => {
34
88
  const text = react.useAuiState(
35
89
  (s) => s.message.content.map((part) => part.type === "text" ? part.text : "").filter(Boolean).join("\n")
@@ -43,8 +97,22 @@ const VoiceDelegationMessage = ({ className }) => {
43
97
  }
44
98
  return false;
45
99
  });
100
+ const echo = react.useAuiState(
101
+ (s) => {
102
+ var _a;
103
+ return ((_a = s.thread.messages[s.message.index]) == null ? void 0 : _a.id) === s.message.id && voicePresentation.voiceDelegationEchoesRequest(s.thread.messages, s.message.index);
104
+ }
105
+ );
46
106
  const isRunning = useAthenaRunLive.useAthenaRunLive();
47
- return /* @__PURE__ */ jsxRuntime.jsx(VoiceDelegationCard, { text, live: isRunning && isTrailingUserMessage, className });
107
+ return /* @__PURE__ */ jsxRuntime.jsx(
108
+ VoiceDelegationCard,
109
+ {
110
+ text,
111
+ live: isRunning && isTrailingUserMessage,
112
+ echo,
113
+ className
114
+ }
115
+ );
48
116
  };
49
117
  exports.VoiceDelegationCard = VoiceDelegationCard;
50
118
  exports.VoiceDelegationMessage = VoiceDelegationMessage;
@@ -1 +1 @@
1
- {"version":3,"file":"voice-delegation-card.cjs","sources":["../../../src/chat-ui/messages/voice-delegation-card.tsx"],"sourcesContent":["\"use client\";\n\nimport { useAuiState } from \"@assistant-ui/react\";\nimport { AudioLinesIcon, LoaderIcon } from \"lucide-react\";\nimport type { FC } from \"react\";\nimport { useAthenaRunLive } from \"../../chat/use-athena-run-live\";\nimport { cn } from \"../../lib/utils\";\n\nexport interface VoiceDelegationCardProps {\n /** The live voice model's plain-language ask (never markdown). */\n text: string;\n /** Whether this delegation is plausibly the one running right now. */\n live: boolean;\n className?: string;\n}\n\n/**\n * The card for a voice delegation's anchor message: the unit of work the live\n * voice model handed to the thread. It replaces the plain user bubble on every\n * surface, so delegated work reads as delegated work instead of \"the user\n * typed this twice\" — the spoken ask itself sits just above it as a spoken\n * turn. The run it dispatched streams below as ordinary messages and tool\n * cards. Pure presenter; {@link VoiceDelegationMessage} binds it to the\n * message in scope.\n */\nexport const VoiceDelegationCard: FC<VoiceDelegationCardProps> = ({ text, live, className }) => (\n <div\n className={cn(\"flex justify-end px-1 pb-0.5\", className)}\n data-testid=\"voice-delegation-card\"\n >\n <div className=\"max-w-[85%] rounded-xl border border-primary/20 bg-primary/5 px-3 py-2\">\n <div className=\"mb-1 flex items-center gap-1.5\">\n <span className=\"inline-flex size-4 items-center justify-center rounded bg-primary/15\">\n <AudioLinesIcon className=\"size-2.5 text-primary\" aria-hidden />\n </span>\n <span className=\"text-[10px] font-medium uppercase tracking-wide text-primary\">\n Voice task\n </span>\n {live ? (\n <span\n className=\"inline-flex items-center gap-1 text-[10px] text-primary/80\"\n data-testid=\"voice-delegation-live\"\n >\n <LoaderIcon className=\"size-2.5 animate-spin\" aria-hidden />\n running\n </span>\n ) : null}\n </div>\n <div className=\"whitespace-pre-wrap break-words text-sm text-foreground\">{text}</div>\n </div>\n </div>\n);\n\n/**\n * {@link VoiceDelegationCard} bound to the assistant-ui message in scope. The\n * live pulse shows only while this delegation is plausibly the one running:\n * the thread is running and no later user turn superseded it.\n */\nexport const VoiceDelegationMessage: FC<{ className?: string }> = ({ className }) => {\n const text = useAuiState((s) =>\n s.message.content\n .map((part) => (part.type === \"text\" ? part.text : \"\"))\n .filter(Boolean)\n .join(\"\\n\"),\n );\n const isTrailingUserMessage = useAuiState((s) => {\n const messages = s.thread.messages;\n for (let index = messages.length - 1; index >= 0; index--) {\n const candidate = messages[index];\n if (candidate?.role !== \"user\") continue;\n return candidate.id === s.message.id;\n }\n return false;\n });\n const isRunning = useAthenaRunLive();\n return (\n <VoiceDelegationCard text={text} live={isRunning && isTrailingUserMessage} className={className} />\n );\n};\n"],"names":["jsx","cn","jsxs","AudioLinesIcon","LoaderIcon","useAuiState","useAthenaRunLive"],"mappings":";;;;;;;AAyBO,MAAM,sBAAoD,CAAC,EAAE,MAAM,MAAM,gBAC9EA,2BAAAA;AAAAA,EAAC;AAAA,EAAA;AAAA,IACC,WAAWC,OAAAA,GAAG,gCAAgC,SAAS;AAAA,IACvD,eAAY;AAAA,IAEZ,UAAAC,2BAAAA,KAAC,OAAA,EAAI,WAAU,0EACb,UAAA;AAAA,MAAAA,2BAAAA,KAAC,OAAA,EAAI,WAAU,kCACb,UAAA;AAAA,QAAAF,2BAAAA,IAAC,QAAA,EAAK,WAAU,wEACd,UAAAA,2BAAAA,IAACG,8BAAe,WAAU,yBAAwB,eAAW,KAAA,CAAC,EAAA,CAChE;AAAA,QACAH,2BAAAA,IAAC,QAAA,EAAK,WAAU,gEAA+D,UAAA,cAE/E;AAAA,QACC,OACCE,2BAAAA;AAAAA,UAAC;AAAA,UAAA;AAAA,YACC,WAAU;AAAA,YACV,eAAY;AAAA,YAEZ,UAAA;AAAA,cAAAF,2BAAAA,IAACI,YAAAA,YAAA,EAAW,WAAU,yBAAwB,eAAW,MAAC;AAAA,cAAE;AAAA,YAAA;AAAA,UAAA;AAAA,QAAA,IAG5D;AAAA,MAAA,GACN;AAAA,MACAJ,2BAAAA,IAAC,OAAA,EAAI,WAAU,2DAA2D,UAAA,KAAA,CAAK;AAAA,IAAA,EAAA,CACjF;AAAA,EAAA;AACF;AAQK,MAAM,yBAAqD,CAAC,EAAE,gBAAgB;AACnF,QAAM,OAAOK,MAAAA;AAAAA,IAAY,CAAC,MACxB,EAAE,QAAQ,QACP,IAAI,CAAC,SAAU,KAAK,SAAS,SAAS,KAAK,OAAO,EAAG,EACrD,OAAO,OAAO,EACd,KAAK,IAAI;AAAA,EAAA;AAEd,QAAM,wBAAwBA,kBAAY,CAAC,MAAM;AAC/C,UAAM,WAAW,EAAE,OAAO;AAC1B,aAAS,QAAQ,SAAS,SAAS,GAAG,SAAS,GAAG,SAAS;AACzD,YAAM,YAAY,SAAS,KAAK;AAChC,WAAI,uCAAW,UAAS,OAAQ;AAChC,aAAO,UAAU,OAAO,EAAE,QAAQ;AAAA,IACpC;AACA,WAAO;AAAA,EACT,CAAC;AACD,QAAM,YAAYC,iBAAAA,iBAAA;AAClB,wCACG,qBAAA,EAAoB,MAAY,MAAM,aAAa,uBAAuB,WAAsB;AAErG;;;"}
1
+ {"version":3,"file":"voice-delegation-card.cjs","sources":["../../../src/chat-ui/messages/voice-delegation-card.tsx"],"sourcesContent":["\"use client\";\n\nimport { useAuiState } from \"@assistant-ui/react\";\nimport { voiceDelegationEchoesRequest } from \"@athenaintel/agent-runtime\";\nimport { AudioLinesIcon, ChevronDownIcon, LoaderIcon } from \"lucide-react\";\nimport { type FC, useId, useState } from \"react\";\nimport { useAthenaRunLive } from \"../../chat/use-athena-run-live\";\nimport { cn } from \"../../lib/utils\";\n\nexport interface VoiceDelegationCardProps {\n /** The live voice model's plain-language ask (never markdown). */\n text: string;\n /** Whether this delegation is plausibly the one running right now. */\n live: boolean;\n /**\n * The person's own turn right above already shows this ask\n * (`voiceDelegationEchoesRequest`): only the header shows, and the task's\n * wording opens on request.\n */\n echo?: boolean;\n className?: string;\n}\n\nconst VoiceDelegationHeading: FC<{ live: boolean }> = ({ live }) => (\n <>\n <span className=\"inline-flex size-4 items-center justify-center rounded bg-primary/15\">\n <AudioLinesIcon className=\"size-2.5 text-primary\" aria-hidden />\n </span>\n <span className=\"text-[10px] font-medium uppercase tracking-wide text-primary\">\n Voice task\n </span>\n {live ? (\n <span\n className=\"inline-flex items-center gap-1 text-[10px] text-primary/80\"\n data-testid=\"voice-delegation-live\"\n >\n <LoaderIcon className=\"size-2.5 animate-spin\" aria-hidden />\n running\n </span>\n ) : null}\n </>\n);\n\n/**\n * The card for a voice delegation's anchor message: the unit of work the live\n * voice model handed to the thread. It replaces the plain user bubble on every\n * surface, so delegated work reads as delegated work, not as the user typing\n * again. Right under the person's own words (`echo`) it is only their request\n * repeated, so it shrinks to its header and the task's wording opens on a tap;\n * otherwise (a follow-up whose spoken turn has not landed above it, a task with\n * no visible ask) it shows the ask in full. The run it dispatched streams below\n * as ordinary messages and tool cards. Pure presenter;\n * {@link VoiceDelegationMessage} binds it to the message in scope.\n */\nexport const VoiceDelegationCard: FC<VoiceDelegationCardProps> = ({\n text,\n live,\n echo = false,\n className,\n}) => {\n const [open, setOpen] = useState(false);\n const textId = useId();\n if (echo) {\n return (\n <div\n className={cn(\"flex flex-col items-end px-1 pb-0.5\", className)}\n data-testid=\"voice-delegation-card\"\n data-echo=\"true\"\n >\n <button\n type=\"button\"\n className=\"inline-flex items-center gap-1.5 rounded-full border border-primary/20 bg-primary/5 px-2 py-0.5 hover:bg-primary/10\"\n aria-expanded={open}\n aria-controls={textId}\n onClick={() => setOpen((value) => !value)}\n data-testid=\"voice-delegation-toggle\"\n >\n <VoiceDelegationHeading live={live} />\n <span className=\"sr-only\">{open ? \"Hide the task\" : \"Show the task\"}</span>\n <ChevronDownIcon\n className={cn(\"size-3 text-primary/70 transition-transform\", open && \"rotate-180\")}\n aria-hidden\n />\n </button>\n {open ? (\n <div\n id={textId}\n className=\"mt-1 max-w-[85%] whitespace-pre-wrap break-words rounded-xl border border-primary/20 bg-primary/5 px-3 py-2 text-sm text-foreground\"\n >\n {text}\n </div>\n ) : null}\n </div>\n );\n }\n return (\n <div\n className={cn(\"flex justify-end px-1 pb-0.5\", className)}\n data-testid=\"voice-delegation-card\"\n >\n <div className=\"max-w-[85%] rounded-xl border border-primary/20 bg-primary/5 px-3 py-2\">\n <div className=\"mb-1 flex items-center gap-1.5\">\n <VoiceDelegationHeading live={live} />\n </div>\n <div className=\"whitespace-pre-wrap break-words text-sm text-foreground\">{text}</div>\n </div>\n </div>\n );\n};\n\n/**\n * {@link VoiceDelegationCard} bound to the assistant-ui message in scope. The\n * live pulse shows only while this delegation is plausibly the one running:\n * the thread is running and no later user turn superseded it. It draws as an\n * echo when the person's own turn right above already shows the ask.\n */\nexport const VoiceDelegationMessage: FC<{ className?: string }> = ({ className }) => {\n const text = useAuiState((s) =>\n s.message.content\n .map((part) => (part.type === \"text\" ? part.text : \"\"))\n .filter(Boolean)\n .join(\"\\n\"),\n );\n const isTrailingUserMessage = useAuiState((s) => {\n const messages = s.thread.messages;\n for (let index = messages.length - 1; index >= 0; index--) {\n const candidate = messages[index];\n if (candidate?.role !== \"user\") continue;\n return candidate.id === s.message.id;\n }\n return false;\n });\n const echo = useAuiState(\n (s) =>\n s.thread.messages[s.message.index]?.id === s.message.id &&\n voiceDelegationEchoesRequest(s.thread.messages, s.message.index),\n );\n const isRunning = useAthenaRunLive();\n return (\n <VoiceDelegationCard\n text={text}\n live={isRunning && isTrailingUserMessage}\n echo={echo}\n className={className}\n />\n );\n};\n"],"names":["jsxs","Fragment","jsx","AudioLinesIcon","LoaderIcon","useState","useId","cn","ChevronDownIcon","useAuiState","voiceDelegationEchoesRequest","useAthenaRunLive"],"mappings":"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"}
@@ -1,25 +1,34 @@
1
- import type { FC } from "react";
1
+ import { type FC } from "react";
2
2
  export interface VoiceDelegationCardProps {
3
3
  /** The live voice model's plain-language ask (never markdown). */
4
4
  text: string;
5
5
  /** Whether this delegation is plausibly the one running right now. */
6
6
  live: boolean;
7
+ /**
8
+ * The person's own turn right above already shows this ask
9
+ * (`voiceDelegationEchoesRequest`): only the header shows, and the task's
10
+ * wording opens on request.
11
+ */
12
+ echo?: boolean;
7
13
  className?: string;
8
14
  }
9
15
  /**
10
16
  * The card for a voice delegation's anchor message: the unit of work the live
11
17
  * voice model handed to the thread. It replaces the plain user bubble on every
12
- * surface, so delegated work reads as delegated work instead of "the user
13
- * typed this twice" — the spoken ask itself sits just above it as a spoken
14
- * turn. The run it dispatched streams below as ordinary messages and tool
15
- * cards. Pure presenter; {@link VoiceDelegationMessage} binds it to the
16
- * message in scope.
18
+ * surface, so delegated work reads as delegated work, not as the user typing
19
+ * again. Right under the person's own words (`echo`) it is only their request
20
+ * repeated, so it shrinks to its header and the task's wording opens on a tap;
21
+ * otherwise (a follow-up whose spoken turn has not landed above it, a task with
22
+ * no visible ask) it shows the ask in full. The run it dispatched streams below
23
+ * as ordinary messages and tool cards. Pure presenter;
24
+ * {@link VoiceDelegationMessage} binds it to the message in scope.
17
25
  */
18
26
  export declare const VoiceDelegationCard: FC<VoiceDelegationCardProps>;
19
27
  /**
20
28
  * {@link VoiceDelegationCard} bound to the assistant-ui message in scope. The
21
29
  * live pulse shows only while this delegation is plausibly the one running:
22
- * the thread is running and no later user turn superseded it.
30
+ * the thread is running and no later user turn superseded it. It draws as an
31
+ * echo when the person's own turn right above already shows the ask.
23
32
  */
24
33
  export declare const VoiceDelegationMessage: FC<{
25
34
  className?: string;
@@ -1,33 +1,87 @@
1
- import { jsx, jsxs } from "react/jsx-runtime";
1
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
2
  import { useAuiState } from "@assistant-ui/react";
3
- import { AudioLinesIcon, LoaderIcon } from "lucide-react";
3
+ import { ChevronDownIcon, AudioLinesIcon, LoaderIcon } from "lucide-react";
4
+ import { useState, useId } from "react";
4
5
  import { useAthenaRunLive } from "../../chat/use-athena-run-live.js";
6
+ import { voiceDelegationEchoesRequest } from "../../agent-runtime/src/voice-presentation.js";
5
7
  import { cn } from "cnfast";
6
- const VoiceDelegationCard = ({ text, live, className }) => /* @__PURE__ */ jsx(
7
- "div",
8
- {
9
- className: cn("flex justify-end px-1 pb-0.5", className),
10
- "data-testid": "voice-delegation-card",
11
- children: /* @__PURE__ */ jsxs("div", { className: "max-w-[85%] rounded-xl border border-primary/20 bg-primary/5 px-3 py-2", children: [
12
- /* @__PURE__ */ jsxs("div", { className: "mb-1 flex items-center gap-1.5", children: [
13
- /* @__PURE__ */ jsx("span", { className: "inline-flex size-4 items-center justify-center rounded bg-primary/15", children: /* @__PURE__ */ jsx(AudioLinesIcon, { className: "size-2.5 text-primary", "aria-hidden": true }) }),
14
- /* @__PURE__ */ jsx("span", { className: "text-[10px] font-medium uppercase tracking-wide text-primary", children: "Voice task" }),
15
- live ? /* @__PURE__ */ jsxs(
16
- "span",
17
- {
18
- className: "inline-flex items-center gap-1 text-[10px] text-primary/80",
19
- "data-testid": "voice-delegation-live",
20
- children: [
21
- /* @__PURE__ */ jsx(LoaderIcon, { className: "size-2.5 animate-spin", "aria-hidden": true }),
22
- "running"
23
- ]
24
- }
25
- ) : null
26
- ] }),
27
- /* @__PURE__ */ jsx("div", { className: "whitespace-pre-wrap break-words text-sm text-foreground", children: text })
28
- ] })
8
+ const VoiceDelegationHeading = ({ live }) => /* @__PURE__ */ jsxs(Fragment, { children: [
9
+ /* @__PURE__ */ jsx("span", { className: "inline-flex size-4 items-center justify-center rounded bg-primary/15", children: /* @__PURE__ */ jsx(AudioLinesIcon, { className: "size-2.5 text-primary", "aria-hidden": true }) }),
10
+ /* @__PURE__ */ jsx("span", { className: "text-[10px] font-medium uppercase tracking-wide text-primary", children: "Voice task" }),
11
+ live ? /* @__PURE__ */ jsxs(
12
+ "span",
13
+ {
14
+ className: "inline-flex items-center gap-1 text-[10px] text-primary/80",
15
+ "data-testid": "voice-delegation-live",
16
+ children: [
17
+ /* @__PURE__ */ jsx(LoaderIcon, { className: "size-2.5 animate-spin", "aria-hidden": true }),
18
+ "running"
19
+ ]
20
+ }
21
+ ) : null
22
+ ] });
23
+ const VoiceDelegationCard = ({
24
+ text,
25
+ live,
26
+ echo = false,
27
+ className
28
+ }) => {
29
+ const [open, setOpen] = useState(false);
30
+ const textId = useId();
31
+ if (echo) {
32
+ return /* @__PURE__ */ jsxs(
33
+ "div",
34
+ {
35
+ className: cn("flex flex-col items-end px-1 pb-0.5", className),
36
+ "data-testid": "voice-delegation-card",
37
+ "data-echo": "true",
38
+ children: [
39
+ /* @__PURE__ */ jsxs(
40
+ "button",
41
+ {
42
+ type: "button",
43
+ className: "inline-flex items-center gap-1.5 rounded-full border border-primary/20 bg-primary/5 px-2 py-0.5 hover:bg-primary/10",
44
+ "aria-expanded": open,
45
+ "aria-controls": textId,
46
+ onClick: () => setOpen((value) => !value),
47
+ "data-testid": "voice-delegation-toggle",
48
+ children: [
49
+ /* @__PURE__ */ jsx(VoiceDelegationHeading, { live }),
50
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: open ? "Hide the task" : "Show the task" }),
51
+ /* @__PURE__ */ jsx(
52
+ ChevronDownIcon,
53
+ {
54
+ className: cn("size-3 text-primary/70 transition-transform", open && "rotate-180"),
55
+ "aria-hidden": true
56
+ }
57
+ )
58
+ ]
59
+ }
60
+ ),
61
+ open ? /* @__PURE__ */ jsx(
62
+ "div",
63
+ {
64
+ id: textId,
65
+ className: "mt-1 max-w-[85%] whitespace-pre-wrap break-words rounded-xl border border-primary/20 bg-primary/5 px-3 py-2 text-sm text-foreground",
66
+ children: text
67
+ }
68
+ ) : null
69
+ ]
70
+ }
71
+ );
29
72
  }
30
- );
73
+ return /* @__PURE__ */ jsx(
74
+ "div",
75
+ {
76
+ className: cn("flex justify-end px-1 pb-0.5", className),
77
+ "data-testid": "voice-delegation-card",
78
+ children: /* @__PURE__ */ jsxs("div", { className: "max-w-[85%] rounded-xl border border-primary/20 bg-primary/5 px-3 py-2", children: [
79
+ /* @__PURE__ */ jsx("div", { className: "mb-1 flex items-center gap-1.5", children: /* @__PURE__ */ jsx(VoiceDelegationHeading, { live }) }),
80
+ /* @__PURE__ */ jsx("div", { className: "whitespace-pre-wrap break-words text-sm text-foreground", children: text })
81
+ ] })
82
+ }
83
+ );
84
+ };
31
85
  const VoiceDelegationMessage = ({ className }) => {
32
86
  const text = useAuiState(
33
87
  (s) => s.message.content.map((part) => part.type === "text" ? part.text : "").filter(Boolean).join("\n")
@@ -41,8 +95,22 @@ const VoiceDelegationMessage = ({ className }) => {
41
95
  }
42
96
  return false;
43
97
  });
98
+ const echo = useAuiState(
99
+ (s) => {
100
+ var _a;
101
+ return ((_a = s.thread.messages[s.message.index]) == null ? void 0 : _a.id) === s.message.id && voiceDelegationEchoesRequest(s.thread.messages, s.message.index);
102
+ }
103
+ );
44
104
  const isRunning = useAthenaRunLive();
45
- return /* @__PURE__ */ jsx(VoiceDelegationCard, { text, live: isRunning && isTrailingUserMessage, className });
105
+ return /* @__PURE__ */ jsx(
106
+ VoiceDelegationCard,
107
+ {
108
+ text,
109
+ live: isRunning && isTrailingUserMessage,
110
+ echo,
111
+ className
112
+ }
113
+ );
46
114
  };
47
115
  export {
48
116
  VoiceDelegationCard,
@@ -1 +1 @@
1
- {"version":3,"file":"voice-delegation-card.js","sources":["../../../src/chat-ui/messages/voice-delegation-card.tsx"],"sourcesContent":["\"use client\";\n\nimport { useAuiState } from \"@assistant-ui/react\";\nimport { AudioLinesIcon, LoaderIcon } from \"lucide-react\";\nimport type { FC } from \"react\";\nimport { useAthenaRunLive } from \"../../chat/use-athena-run-live\";\nimport { cn } from \"../../lib/utils\";\n\nexport interface VoiceDelegationCardProps {\n /** The live voice model's plain-language ask (never markdown). */\n text: string;\n /** Whether this delegation is plausibly the one running right now. */\n live: boolean;\n className?: string;\n}\n\n/**\n * The card for a voice delegation's anchor message: the unit of work the live\n * voice model handed to the thread. It replaces the plain user bubble on every\n * surface, so delegated work reads as delegated work instead of \"the user\n * typed this twice\" — the spoken ask itself sits just above it as a spoken\n * turn. The run it dispatched streams below as ordinary messages and tool\n * cards. Pure presenter; {@link VoiceDelegationMessage} binds it to the\n * message in scope.\n */\nexport const VoiceDelegationCard: FC<VoiceDelegationCardProps> = ({ text, live, className }) => (\n <div\n className={cn(\"flex justify-end px-1 pb-0.5\", className)}\n data-testid=\"voice-delegation-card\"\n >\n <div className=\"max-w-[85%] rounded-xl border border-primary/20 bg-primary/5 px-3 py-2\">\n <div className=\"mb-1 flex items-center gap-1.5\">\n <span className=\"inline-flex size-4 items-center justify-center rounded bg-primary/15\">\n <AudioLinesIcon className=\"size-2.5 text-primary\" aria-hidden />\n </span>\n <span className=\"text-[10px] font-medium uppercase tracking-wide text-primary\">\n Voice task\n </span>\n {live ? (\n <span\n className=\"inline-flex items-center gap-1 text-[10px] text-primary/80\"\n data-testid=\"voice-delegation-live\"\n >\n <LoaderIcon className=\"size-2.5 animate-spin\" aria-hidden />\n running\n </span>\n ) : null}\n </div>\n <div className=\"whitespace-pre-wrap break-words text-sm text-foreground\">{text}</div>\n </div>\n </div>\n);\n\n/**\n * {@link VoiceDelegationCard} bound to the assistant-ui message in scope. The\n * live pulse shows only while this delegation is plausibly the one running:\n * the thread is running and no later user turn superseded it.\n */\nexport const VoiceDelegationMessage: FC<{ className?: string }> = ({ className }) => {\n const text = useAuiState((s) =>\n s.message.content\n .map((part) => (part.type === \"text\" ? part.text : \"\"))\n .filter(Boolean)\n .join(\"\\n\"),\n );\n const isTrailingUserMessage = useAuiState((s) => {\n const messages = s.thread.messages;\n for (let index = messages.length - 1; index >= 0; index--) {\n const candidate = messages[index];\n if (candidate?.role !== \"user\") continue;\n return candidate.id === s.message.id;\n }\n return false;\n });\n const isRunning = useAthenaRunLive();\n return (\n <VoiceDelegationCard text={text} live={isRunning && isTrailingUserMessage} className={className} />\n );\n};\n"],"names":[],"mappings":";;;;;AAyBO,MAAM,sBAAoD,CAAC,EAAE,MAAM,MAAM,gBAC9E;AAAA,EAAC;AAAA,EAAA;AAAA,IACC,WAAW,GAAG,gCAAgC,SAAS;AAAA,IACvD,eAAY;AAAA,IAEZ,UAAA,qBAAC,OAAA,EAAI,WAAU,0EACb,UAAA;AAAA,MAAA,qBAAC,OAAA,EAAI,WAAU,kCACb,UAAA;AAAA,QAAA,oBAAC,QAAA,EAAK,WAAU,wEACd,UAAA,oBAAC,kBAAe,WAAU,yBAAwB,eAAW,KAAA,CAAC,EAAA,CAChE;AAAA,QACA,oBAAC,QAAA,EAAK,WAAU,gEAA+D,UAAA,cAE/E;AAAA,QACC,OACC;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,WAAU;AAAA,YACV,eAAY;AAAA,YAEZ,UAAA;AAAA,cAAA,oBAAC,YAAA,EAAW,WAAU,yBAAwB,eAAW,MAAC;AAAA,cAAE;AAAA,YAAA;AAAA,UAAA;AAAA,QAAA,IAG5D;AAAA,MAAA,GACN;AAAA,MACA,oBAAC,OAAA,EAAI,WAAU,2DAA2D,UAAA,KAAA,CAAK;AAAA,IAAA,EAAA,CACjF;AAAA,EAAA;AACF;AAQK,MAAM,yBAAqD,CAAC,EAAE,gBAAgB;AACnF,QAAM,OAAO;AAAA,IAAY,CAAC,MACxB,EAAE,QAAQ,QACP,IAAI,CAAC,SAAU,KAAK,SAAS,SAAS,KAAK,OAAO,EAAG,EACrD,OAAO,OAAO,EACd,KAAK,IAAI;AAAA,EAAA;AAEd,QAAM,wBAAwB,YAAY,CAAC,MAAM;AAC/C,UAAM,WAAW,EAAE,OAAO;AAC1B,aAAS,QAAQ,SAAS,SAAS,GAAG,SAAS,GAAG,SAAS;AACzD,YAAM,YAAY,SAAS,KAAK;AAChC,WAAI,uCAAW,UAAS,OAAQ;AAChC,aAAO,UAAU,OAAO,EAAE,QAAQ;AAAA,IACpC;AACA,WAAO;AAAA,EACT,CAAC;AACD,QAAM,YAAY,iBAAA;AAClB,6BACG,qBAAA,EAAoB,MAAY,MAAM,aAAa,uBAAuB,WAAsB;AAErG;"}
1
+ {"version":3,"file":"voice-delegation-card.js","sources":["../../../src/chat-ui/messages/voice-delegation-card.tsx"],"sourcesContent":["\"use client\";\n\nimport { useAuiState } from \"@assistant-ui/react\";\nimport { voiceDelegationEchoesRequest } from \"@athenaintel/agent-runtime\";\nimport { AudioLinesIcon, ChevronDownIcon, LoaderIcon } from \"lucide-react\";\nimport { type FC, useId, useState } from \"react\";\nimport { useAthenaRunLive } from \"../../chat/use-athena-run-live\";\nimport { cn } from \"../../lib/utils\";\n\nexport interface VoiceDelegationCardProps {\n /** The live voice model's plain-language ask (never markdown). */\n text: string;\n /** Whether this delegation is plausibly the one running right now. */\n live: boolean;\n /**\n * The person's own turn right above already shows this ask\n * (`voiceDelegationEchoesRequest`): only the header shows, and the task's\n * wording opens on request.\n */\n echo?: boolean;\n className?: string;\n}\n\nconst VoiceDelegationHeading: FC<{ live: boolean }> = ({ live }) => (\n <>\n <span className=\"inline-flex size-4 items-center justify-center rounded bg-primary/15\">\n <AudioLinesIcon className=\"size-2.5 text-primary\" aria-hidden />\n </span>\n <span className=\"text-[10px] font-medium uppercase tracking-wide text-primary\">\n Voice task\n </span>\n {live ? (\n <span\n className=\"inline-flex items-center gap-1 text-[10px] text-primary/80\"\n data-testid=\"voice-delegation-live\"\n >\n <LoaderIcon className=\"size-2.5 animate-spin\" aria-hidden />\n running\n </span>\n ) : null}\n </>\n);\n\n/**\n * The card for a voice delegation's anchor message: the unit of work the live\n * voice model handed to the thread. It replaces the plain user bubble on every\n * surface, so delegated work reads as delegated work, not as the user typing\n * again. Right under the person's own words (`echo`) it is only their request\n * repeated, so it shrinks to its header and the task's wording opens on a tap;\n * otherwise (a follow-up whose spoken turn has not landed above it, a task with\n * no visible ask) it shows the ask in full. The run it dispatched streams below\n * as ordinary messages and tool cards. Pure presenter;\n * {@link VoiceDelegationMessage} binds it to the message in scope.\n */\nexport const VoiceDelegationCard: FC<VoiceDelegationCardProps> = ({\n text,\n live,\n echo = false,\n className,\n}) => {\n const [open, setOpen] = useState(false);\n const textId = useId();\n if (echo) {\n return (\n <div\n className={cn(\"flex flex-col items-end px-1 pb-0.5\", className)}\n data-testid=\"voice-delegation-card\"\n data-echo=\"true\"\n >\n <button\n type=\"button\"\n className=\"inline-flex items-center gap-1.5 rounded-full border border-primary/20 bg-primary/5 px-2 py-0.5 hover:bg-primary/10\"\n aria-expanded={open}\n aria-controls={textId}\n onClick={() => setOpen((value) => !value)}\n data-testid=\"voice-delegation-toggle\"\n >\n <VoiceDelegationHeading live={live} />\n <span className=\"sr-only\">{open ? \"Hide the task\" : \"Show the task\"}</span>\n <ChevronDownIcon\n className={cn(\"size-3 text-primary/70 transition-transform\", open && \"rotate-180\")}\n aria-hidden\n />\n </button>\n {open ? (\n <div\n id={textId}\n className=\"mt-1 max-w-[85%] whitespace-pre-wrap break-words rounded-xl border border-primary/20 bg-primary/5 px-3 py-2 text-sm text-foreground\"\n >\n {text}\n </div>\n ) : null}\n </div>\n );\n }\n return (\n <div\n className={cn(\"flex justify-end px-1 pb-0.5\", className)}\n data-testid=\"voice-delegation-card\"\n >\n <div className=\"max-w-[85%] rounded-xl border border-primary/20 bg-primary/5 px-3 py-2\">\n <div className=\"mb-1 flex items-center gap-1.5\">\n <VoiceDelegationHeading live={live} />\n </div>\n <div className=\"whitespace-pre-wrap break-words text-sm text-foreground\">{text}</div>\n </div>\n </div>\n );\n};\n\n/**\n * {@link VoiceDelegationCard} bound to the assistant-ui message in scope. The\n * live pulse shows only while this delegation is plausibly the one running:\n * the thread is running and no later user turn superseded it. It draws as an\n * echo when the person's own turn right above already shows the ask.\n */\nexport const VoiceDelegationMessage: FC<{ className?: string }> = ({ className }) => {\n const text = useAuiState((s) =>\n s.message.content\n .map((part) => (part.type === \"text\" ? part.text : \"\"))\n .filter(Boolean)\n .join(\"\\n\"),\n );\n const isTrailingUserMessage = useAuiState((s) => {\n const messages = s.thread.messages;\n for (let index = messages.length - 1; index >= 0; index--) {\n const candidate = messages[index];\n if (candidate?.role !== \"user\") continue;\n return candidate.id === s.message.id;\n }\n return false;\n });\n const echo = useAuiState(\n (s) =>\n s.thread.messages[s.message.index]?.id === s.message.id &&\n voiceDelegationEchoesRequest(s.thread.messages, s.message.index),\n );\n const isRunning = useAthenaRunLive();\n return (\n <VoiceDelegationCard\n text={text}\n live={isRunning && isTrailingUserMessage}\n echo={echo}\n className={className}\n />\n );\n};\n"],"names":[],"mappings":"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"}
@@ -117,8 +117,8 @@ function startHandoffCursorReveal({
117
117
  }
118
118
  pagesLoaded += 1;
119
119
  waitingSince = now();
120
- }).catch(() => {
121
- pagesLoaded = Number.POSITIVE_INFINITY;
120
+ }).catch((error) => {
121
+ if (!editCommand.isHistoryPageLostToReconnect(error)) pagesLoaded = Number.POSITIVE_INFINITY;
122
122
  }).finally(() => {
123
123
  paging = false;
124
124
  pageLandsAt = now() + 2 * HANDOFF_CURSOR_POLL_MS;
@@ -1 +1 @@
1
- {"version":3,"file":"handoff-cursor.cjs","sources":["../../../src/chat-ui/thread/handoff-cursor.tsx"],"sourcesContent":["/**\n * Handovers open where the person was reading (`@athenaintel/agent-runtime`'s\n * `handoff-cursor`), for every host of `ChatThreadShell` (Athena in Chrome,\n * the desktop app, `AthenaChat`).\n *\n * As a source, `useReadThreadReadingCursor` reads the shell's viewport at the\n * moment of the click: `null` at the bottom, else the id of the last message\n * fully in view. As a destination, `HandoffCursorReveal` (mounted by the shell\n * for its `arrivalThreadId`) scrolls to the message the arrival names once the\n * transcript has it, paging older history in when it is further back, and\n * highlights it briefly; a cursor it can't find leaves the view at the bottom.\n */\nimport {\n type ThreadMessage,\n type ThreadViewportState,\n useAui,\n useThreadViewport,\n useThreadViewportStore,\n} from \"@assistant-ui/react\";\nimport {\n deepAgentHistoryHasOlder,\n HANDOFF_CURSOR_HIGHLIGHT_MS,\n HANDOFF_CURSOR_SETTLE_MS,\n handoffCursorMessageId,\n handoffCursorRevealStep,\n loadOlderDeepAgentHistoryPage,\n MESSAGE_ROW_ID_ATTRIBUTE,\n readDeepAgentStatewireExtras,\n readHandoffArrival,\n readingCursorOf,\n settleHandoffArrivalCursor,\n useHandoffArrivalCursor,\n} from \"@athenaintel/agent-runtime\";\nimport type { FC } from \"react\";\nimport { useCallback, useEffect, useRef } from \"react\";\nimport { THREAD_VIEWPORT_ATTR, THREAD_VIEWPORT_FOOTER_ATTR } from \"./statewire-history-edge\";\n/** What assistant-ui's `ThreadPrimitive.Root` stamps on the thread panel. */\nconst THREAD_ROOT_SELECTOR = \"[data-aui-thread-root]\";\n/** Set on the revealed message while it is highlighted. */\nexport const HANDOFF_CURSOR_REVEALED_ATTR = \"data-handoff-cursor-revealed\";\n/** How often a landing looks for its message while the transcript loads. */\nexport const HANDOFF_CURSOR_POLL_MS = 150;\n\nconst escapeAttributeValue = (value: string): string =>\n typeof CSS !== \"undefined\" && typeof CSS.escape === \"function\"\n ? CSS.escape(value)\n : value.replace(/[\"\\\\]/g, \"\\\\$&\");\n\n/** The thread viewport `anchor` sits in, or the one of the thread panel it sits in. */\nexport function findThreadViewport(anchor: Element | null | undefined): HTMLElement | null {\n if (!anchor) return null;\n const inside = anchor.closest<HTMLElement>(`[${THREAD_VIEWPORT_ATTR}]`);\n if (inside) return inside;\n return (\n anchor.closest(THREAD_ROOT_SELECTOR)?.querySelector<HTMLElement>(`[${THREAD_VIEWPORT_ATTR}]`) ??\n null\n );\n}\n\n/** How much of `viewport` its sticky footer (the composer dock) covers at the bottom. */\nfunction coveredBottomOf(viewport: HTMLElement): number {\n const footer = viewport.querySelector<HTMLElement>(`[${THREAD_VIEWPORT_FOOTER_ATTR}]`);\n if (!footer) return 0;\n return Math.max(0, viewport.getBoundingClientRect().bottom - footer.getBoundingClientRect().top);\n}\n\n/** Where `viewport` is being read: `null` at the bottom, else a message id. */\nexport function readThreadReadingCursor(viewport: HTMLElement | null): string | null {\n if (!viewport) return null;\n return readingCursorOf({ viewport, coveredBottomPx: coveredBottomOf(viewport) });\n}\n\n/**\n * Reads where the thread is being read, at the moment of a click: inside the\n * viewport (a composer toolbar, even from a portaled dialog) through the\n * thread's viewport store; elsewhere in the panel (a header menu) from the\n * DOM around `anchor`. `null` at the bottom or with no viewport found.\n */\nexport function useReadThreadReadingCursor(): (anchor?: Element | null) => string | null {\n const store = useThreadViewportStore({ optional: true });\n return useCallback(\n (anchor?: Element | null) =>\n readThreadReadingCursor(store?.getState().element.viewport ?? findThreadViewport(anchor)),\n [store],\n );\n}\n\n/** Scrolls `element` into `viewport`'s visible part: centred, or its top when taller. */\nfunction scrollMessageIntoView({\n viewport,\n element,\n}: {\n viewport: HTMLElement;\n element: HTMLElement;\n}): void {\n const box = viewport.getBoundingClientRect();\n const rect = element.getBoundingClientRect();\n const visible = Math.max(1, box.height - coveredBottomOf(viewport));\n const offsetTop = rect.top - box.top + viewport.scrollTop;\n const top =\n rect.height >= visible ? offsetTop - 8 : offsetTop - (visible - rect.height) / 2;\n // `instant`: the shell's viewport scrolls smoothly by default.\n viewport.scrollTo({ top: Math.max(0, top), behavior: \"instant\" });\n}\n\nfunction isInView({ viewport, element }: { viewport: HTMLElement; element: HTMLElement }): boolean {\n const box = viewport.getBoundingClientRect();\n const rect = element.getBoundingClientRect();\n return rect.bottom > box.top && rect.top < box.bottom - coveredBottomOf(viewport);\n}\n\n/**\n * A brief tint on the revealed message, through the Web Animations API so it\n * needs nothing from the host's stylesheet. Returns its undo.\n */\nfunction highlightMessage(element: HTMLElement): () => void {\n element.setAttribute(HANDOFF_CURSOR_REVEALED_ATTR, \"\");\n const animation =\n typeof element.animate === \"function\"\n ? element.animate(\n [\n {\n backgroundColor: \"rgba(37, 99, 235, 0.12)\",\n boxShadow: \"0 0 0 2px rgba(37, 99, 235, 0.35)\",\n borderRadius: \"12px\",\n },\n {\n backgroundColor: \"rgba(37, 99, 235, 0.12)\",\n boxShadow: \"0 0 0 2px rgba(37, 99, 235, 0.35)\",\n borderRadius: \"12px\",\n offset: 0.5,\n },\n {\n backgroundColor: \"rgba(37, 99, 235, 0)\",\n boxShadow: \"0 0 0 2px rgba(37, 99, 235, 0)\",\n borderRadius: \"12px\",\n },\n ],\n { duration: HANDOFF_CURSOR_HIGHLIGHT_MS, easing: \"ease-out\" },\n )\n : null;\n const timer = setTimeout(\n () => element.removeAttribute(HANDOFF_CURSOR_REVEALED_ATTR),\n HANDOFF_CURSOR_HIGHLIGHT_MS,\n );\n return () => {\n clearTimeout(timer);\n animation?.cancel();\n element.removeAttribute(HANDOFF_CURSOR_REVEALED_ATTR);\n };\n}\n\ninterface ThreadSnapshot {\n messages: readonly ThreadMessage[];\n isLoading: boolean;\n extras: unknown;\n}\n\n/**\n * Opens the transcript at `cursor`: looks for the message every\n * {@link HANDOFF_CURSOR_POLL_MS}, pages older history in while it is further\n * back, scrolls to it, holds it in view while the transcript settles (until\n * the person scrolls), and highlights it. `claim` takes the pickup right\n * before the scroll (`false`: another transcript showing the conversation took\n * it, and this one stays put). Calls `onSettled` once it gave up (the bottom)\n * or its hold ended. Returns its cancel, which leaves a revealed message's\n * hold and highlight to finish on their own unless `endHold` (a newer pickup\n * took over).\n */\nexport function startHandoffCursorReveal({\n cursor,\n readThread,\n readViewport,\n claim = () => true,\n onSettled,\n now = () => Date.now(),\n}: {\n cursor: string;\n readThread: () => ThreadSnapshot;\n readViewport: () => HTMLElement | null;\n claim?: () => boolean;\n onSettled: () => void;\n now?: () => number;\n}): (options?: { endHold?: boolean }) => void {\n // Progress restarts the wait: the reveal started, or a page of older history landed.\n let waitingSince = now();\n let pagesLoaded = 0;\n let paging = false;\n // A page that landed renders on a later commit: look again before paging on.\n let pageLandsAt = 0;\n let settled = false;\n let revealed: { element: HTMLElement; viewport: HTMLElement; at: number } | null = null;\n let personScrolled = false;\n let unhighlight: (() => void) | null = null;\n let detach: (() => void) | null = null;\n\n const settle = () => {\n if (settled) return;\n settled = true;\n clearInterval(timer);\n detach?.();\n onSettled();\n };\n const pageOlder = () => {\n paging = true;\n loadOlderDeepAgentHistoryPage({\n readExtras: () => readDeepAgentStatewireExtras(readThread().extras),\n })\n .then((loaded) => {\n if (!loaded) {\n // Nothing older could be stated: the history is exhausted.\n pagesLoaded = Number.POSITIVE_INFINITY;\n return;\n }\n pagesLoaded += 1;\n waitingSince = now();\n })\n .catch(() => {\n pagesLoaded = Number.POSITIVE_INFINITY;\n })\n .finally(() => {\n paging = false;\n pageLandsAt = now() + 2 * HANDOFF_CURSOR_POLL_MS;\n });\n };\n const tick = () => {\n if (settled) return;\n if (revealed) {\n if (personScrolled || now() - revealed.at >= HANDOFF_CURSOR_SETTLE_MS) {\n settle();\n return;\n }\n if (revealed.element.isConnected && !isInView(revealed)) scrollMessageIntoView(revealed);\n return;\n }\n const viewport = readViewport();\n const { messages, isLoading, extras } = readThread();\n const targetId = handoffCursorMessageId({ messages, cursor });\n const element =\n targetId !== null && viewport\n ? viewport.querySelector<HTMLElement>(\n `[${MESSAGE_ROW_ID_ATTRIBUTE}=\"${escapeAttributeValue(targetId)}\"]`,\n )\n : null;\n const step = handoffCursorRevealStep({\n found: element !== null,\n // Still attaching, a page in flight, or the row not rendered yet.\n loading:\n isLoading ||\n paging ||\n now() < pageLandsAt ||\n messages.length === 0 ||\n (targetId !== null && !element),\n hasOlder: deepAgentHistoryHasOlder(extras),\n pagesLoaded,\n elapsedMs: now() - waitingSince,\n });\n switch (step) {\n case \"reveal\": {\n if (!viewport || !element) return;\n if (!claim()) {\n // Another transcript showing this conversation took the pickup.\n settled = true;\n clearInterval(timer);\n return;\n }\n scrollMessageIntoView({ viewport, element });\n unhighlight = highlightMessage(element);\n revealed = { element, viewport, at: now() };\n const stop = () => {\n personScrolled = true;\n };\n const events = [\"wheel\", \"touchstart\", \"pointerdown\", \"keydown\"] as const;\n for (const event of events) viewport.addEventListener(event, stop, { passive: true });\n detach = () => {\n for (const event of events) viewport.removeEventListener(event, stop);\n };\n return;\n }\n case \"page\":\n pageOlder();\n return;\n case \"wait\":\n return;\n case \"bottom\":\n settle();\n return;\n }\n };\n const timer = setInterval(tick, HANDOFF_CURSOR_POLL_MS);\n tick();\n return ({ endHold = false } = {}) => {\n if (endHold) unhighlight?.();\n if (settled) return;\n // A revealed message's hold finishes on its own timer, unless a newer pickup took over.\n if (revealed && !endHold) return;\n settled = true;\n clearInterval(timer);\n detach?.();\n unhighlight?.();\n };\n}\n\n/**\n * Opens `threadId`'s transcript at the message the source was reading, once,\n * when this surface picked the conversation up with a cursor\n * (`recordHandoffArrival({ cursor })`). When two transcripts show the\n * conversation, the first to find the message owns the pickup. Renders nothing.\n */\nexport const HandoffCursorReveal: FC<{ threadId: string | null | undefined }> = ({ threadId }) => {\n const cursor = useHandoffArrivalCursor(threadId);\n const aui = useAui();\n const viewport = useThreadViewport({\n optional: true,\n selector: (state: ThreadViewportState) => state.element.viewport,\n });\n const viewportRef = useRef(viewport);\n viewportRef.current = viewport;\n // The latest pickup's reveal: its hold outlives the claim that settles its cursor.\n const latestRef = useRef<ReturnType<typeof startHandoffCursorReveal> | null>(null);\n useEffect(() => {\n if (!threadId || !cursor) return undefined;\n // A newer pickup takes over: the previous message stops being held and highlighted.\n latestRef.current?.({ endHold: true });\n const cancel = startHandoffCursorReveal({\n cursor,\n readThread: () => {\n const state = aui.thread().getState();\n return { messages: state.messages, isLoading: state.isLoading, extras: state.extras };\n },\n readViewport: () => viewportRef.current ?? null,\n // Settled before the scroll, so a second transcript's reveal stops at its claim.\n claim: () => {\n if (readHandoffArrival(threadId)?.cursor !== cursor) return false;\n settleHandoffArrivalCursor(threadId);\n return true;\n },\n // Only this pickup's: a revealed message's hold ends after its claim\n // settled the cursor, when a later handover may have recorded another.\n onSettled: () => {\n if (readHandoffArrival(threadId)?.cursor === cursor) settleHandoffArrivalCursor(threadId);\n },\n });\n latestRef.current = cancel;\n return cancel;\n }, [aui, cursor, threadId]);\n return null;\n};\n"],"names":["THREAD_VIEWPORT_ATTR","THREAD_VIEWPORT_FOOTER_ATTR","readingCursorOf","useThreadViewportStore","useCallback","HANDOFF_CURSOR_HIGHLIGHT_MS","loadOlderDeepAgentHistoryPage","readDeepAgentStatewireExtras","HANDOFF_CURSOR_SETTLE_MS","handoffCursorMessageId","MESSAGE_ROW_ID_ATTRIBUTE","handoffCursorRevealStep","deepAgentHistoryHasOlder","useHandoffArrivalCursor","useAui","useThreadViewport","useRef","useEffect","readHandoffArrival","settleHandoffArrivalCursor"],"mappings":"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"}
1
+ {"version":3,"file":"handoff-cursor.cjs","sources":["../../../src/chat-ui/thread/handoff-cursor.tsx"],"sourcesContent":["/**\n * Handovers open where the person was reading (`@athenaintel/agent-runtime`'s\n * `handoff-cursor`), for every host of `ChatThreadShell` (Athena in Chrome,\n * the desktop app, `AthenaChat`).\n *\n * As a source, `useReadThreadReadingCursor` reads the shell's viewport at the\n * moment of the click: `null` at the bottom, else the id of the last message\n * fully in view. As a destination, `HandoffCursorReveal` (mounted by the shell\n * for its `arrivalThreadId`) scrolls to the message the arrival names once the\n * transcript has it, paging older history in when it is further back, and\n * highlights it briefly; a cursor it can't find leaves the view at the bottom.\n */\nimport {\n type ThreadMessage,\n type ThreadViewportState,\n useAui,\n useThreadViewport,\n useThreadViewportStore,\n} from \"@assistant-ui/react\";\nimport {\n deepAgentHistoryHasOlder,\n HANDOFF_CURSOR_HIGHLIGHT_MS,\n HANDOFF_CURSOR_SETTLE_MS,\n handoffCursorMessageId,\n handoffCursorRevealStep,\n isHistoryPageLostToReconnect,\n loadOlderDeepAgentHistoryPage,\n MESSAGE_ROW_ID_ATTRIBUTE,\n readDeepAgentStatewireExtras,\n readHandoffArrival,\n readingCursorOf,\n settleHandoffArrivalCursor,\n useHandoffArrivalCursor,\n} from \"@athenaintel/agent-runtime\";\nimport type { FC } from \"react\";\nimport { useCallback, useEffect, useRef } from \"react\";\nimport { THREAD_VIEWPORT_ATTR, THREAD_VIEWPORT_FOOTER_ATTR } from \"./statewire-history-edge\";\n/** What assistant-ui's `ThreadPrimitive.Root` stamps on the thread panel. */\nconst THREAD_ROOT_SELECTOR = \"[data-aui-thread-root]\";\n/** Set on the revealed message while it is highlighted. */\nexport const HANDOFF_CURSOR_REVEALED_ATTR = \"data-handoff-cursor-revealed\";\n/** How often a landing looks for its message while the transcript loads. */\nexport const HANDOFF_CURSOR_POLL_MS = 150;\n\nconst escapeAttributeValue = (value: string): string =>\n typeof CSS !== \"undefined\" && typeof CSS.escape === \"function\"\n ? CSS.escape(value)\n : value.replace(/[\"\\\\]/g, \"\\\\$&\");\n\n/** The thread viewport `anchor` sits in, or the one of the thread panel it sits in. */\nexport function findThreadViewport(anchor: Element | null | undefined): HTMLElement | null {\n if (!anchor) return null;\n const inside = anchor.closest<HTMLElement>(`[${THREAD_VIEWPORT_ATTR}]`);\n if (inside) return inside;\n return (\n anchor.closest(THREAD_ROOT_SELECTOR)?.querySelector<HTMLElement>(`[${THREAD_VIEWPORT_ATTR}]`) ??\n null\n );\n}\n\n/** How much of `viewport` its sticky footer (the composer dock) covers at the bottom. */\nfunction coveredBottomOf(viewport: HTMLElement): number {\n const footer = viewport.querySelector<HTMLElement>(`[${THREAD_VIEWPORT_FOOTER_ATTR}]`);\n if (!footer) return 0;\n return Math.max(0, viewport.getBoundingClientRect().bottom - footer.getBoundingClientRect().top);\n}\n\n/** Where `viewport` is being read: `null` at the bottom, else a message id. */\nexport function readThreadReadingCursor(viewport: HTMLElement | null): string | null {\n if (!viewport) return null;\n return readingCursorOf({ viewport, coveredBottomPx: coveredBottomOf(viewport) });\n}\n\n/**\n * Reads where the thread is being read, at the moment of a click: inside the\n * viewport (a composer toolbar, even from a portaled dialog) through the\n * thread's viewport store; elsewhere in the panel (a header menu) from the\n * DOM around `anchor`. `null` at the bottom or with no viewport found.\n */\nexport function useReadThreadReadingCursor(): (anchor?: Element | null) => string | null {\n const store = useThreadViewportStore({ optional: true });\n return useCallback(\n (anchor?: Element | null) =>\n readThreadReadingCursor(store?.getState().element.viewport ?? findThreadViewport(anchor)),\n [store],\n );\n}\n\n/** Scrolls `element` into `viewport`'s visible part: centred, or its top when taller. */\nfunction scrollMessageIntoView({\n viewport,\n element,\n}: {\n viewport: HTMLElement;\n element: HTMLElement;\n}): void {\n const box = viewport.getBoundingClientRect();\n const rect = element.getBoundingClientRect();\n const visible = Math.max(1, box.height - coveredBottomOf(viewport));\n const offsetTop = rect.top - box.top + viewport.scrollTop;\n const top =\n rect.height >= visible ? offsetTop - 8 : offsetTop - (visible - rect.height) / 2;\n // `instant`: the shell's viewport scrolls smoothly by default.\n viewport.scrollTo({ top: Math.max(0, top), behavior: \"instant\" });\n}\n\nfunction isInView({ viewport, element }: { viewport: HTMLElement; element: HTMLElement }): boolean {\n const box = viewport.getBoundingClientRect();\n const rect = element.getBoundingClientRect();\n return rect.bottom > box.top && rect.top < box.bottom - coveredBottomOf(viewport);\n}\n\n/**\n * A brief tint on the revealed message, through the Web Animations API so it\n * needs nothing from the host's stylesheet. Returns its undo.\n */\nfunction highlightMessage(element: HTMLElement): () => void {\n element.setAttribute(HANDOFF_CURSOR_REVEALED_ATTR, \"\");\n const animation =\n typeof element.animate === \"function\"\n ? element.animate(\n [\n {\n backgroundColor: \"rgba(37, 99, 235, 0.12)\",\n boxShadow: \"0 0 0 2px rgba(37, 99, 235, 0.35)\",\n borderRadius: \"12px\",\n },\n {\n backgroundColor: \"rgba(37, 99, 235, 0.12)\",\n boxShadow: \"0 0 0 2px rgba(37, 99, 235, 0.35)\",\n borderRadius: \"12px\",\n offset: 0.5,\n },\n {\n backgroundColor: \"rgba(37, 99, 235, 0)\",\n boxShadow: \"0 0 0 2px rgba(37, 99, 235, 0)\",\n borderRadius: \"12px\",\n },\n ],\n { duration: HANDOFF_CURSOR_HIGHLIGHT_MS, easing: \"ease-out\" },\n )\n : null;\n const timer = setTimeout(\n () => element.removeAttribute(HANDOFF_CURSOR_REVEALED_ATTR),\n HANDOFF_CURSOR_HIGHLIGHT_MS,\n );\n return () => {\n clearTimeout(timer);\n animation?.cancel();\n element.removeAttribute(HANDOFF_CURSOR_REVEALED_ATTR);\n };\n}\n\ninterface ThreadSnapshot {\n messages: readonly ThreadMessage[];\n isLoading: boolean;\n extras: unknown;\n}\n\n/**\n * Opens the transcript at `cursor`: looks for the message every\n * {@link HANDOFF_CURSOR_POLL_MS}, pages older history in while it is further\n * back, scrolls to it, holds it in view while the transcript settles (until\n * the person scrolls), and highlights it. `claim` takes the pickup right\n * before the scroll (`false`: another transcript showing the conversation took\n * it, and this one stays put). Calls `onSettled` once it gave up (the bottom)\n * or its hold ended. Returns its cancel, which leaves a revealed message's\n * hold and highlight to finish on their own unless `endHold` (a newer pickup\n * took over).\n */\nexport function startHandoffCursorReveal({\n cursor,\n readThread,\n readViewport,\n claim = () => true,\n onSettled,\n now = () => Date.now(),\n}: {\n cursor: string;\n readThread: () => ThreadSnapshot;\n readViewport: () => HTMLElement | null;\n claim?: () => boolean;\n onSettled: () => void;\n now?: () => number;\n}): (options?: { endHold?: boolean }) => void {\n // Progress restarts the wait: the reveal started, or a page of older history landed.\n let waitingSince = now();\n let pagesLoaded = 0;\n let paging = false;\n // A page that landed renders on a later commit: look again before paging on.\n let pageLandsAt = 0;\n let settled = false;\n let revealed: { element: HTMLElement; viewport: HTMLElement; at: number } | null = null;\n let personScrolled = false;\n let unhighlight: (() => void) | null = null;\n let detach: (() => void) | null = null;\n\n const settle = () => {\n if (settled) return;\n settled = true;\n clearInterval(timer);\n detach?.();\n onSettled();\n };\n const pageOlder = () => {\n paging = true;\n loadOlderDeepAgentHistoryPage({\n readExtras: () => readDeepAgentStatewireExtras(readThread().extras),\n })\n .then((loaded) => {\n if (!loaded) {\n // Nothing older could be stated: the history is exhausted.\n pagesLoaded = Number.POSITIVE_INFINITY;\n return;\n }\n pagesLoaded += 1;\n waitingSince = now();\n })\n .catch((error: unknown) => {\n // A dropped connection is not the end of the history: the re-attach\n // carries what was loaded, and the next look pages on from there.\n if (!isHistoryPageLostToReconnect(error)) pagesLoaded = Number.POSITIVE_INFINITY;\n })\n .finally(() => {\n paging = false;\n pageLandsAt = now() + 2 * HANDOFF_CURSOR_POLL_MS;\n });\n };\n const tick = () => {\n if (settled) return;\n if (revealed) {\n if (personScrolled || now() - revealed.at >= HANDOFF_CURSOR_SETTLE_MS) {\n settle();\n return;\n }\n if (revealed.element.isConnected && !isInView(revealed)) scrollMessageIntoView(revealed);\n return;\n }\n const viewport = readViewport();\n const { messages, isLoading, extras } = readThread();\n const targetId = handoffCursorMessageId({ messages, cursor });\n const element =\n targetId !== null && viewport\n ? viewport.querySelector<HTMLElement>(\n `[${MESSAGE_ROW_ID_ATTRIBUTE}=\"${escapeAttributeValue(targetId)}\"]`,\n )\n : null;\n const step = handoffCursorRevealStep({\n found: element !== null,\n // Still attaching, a page in flight, or the row not rendered yet.\n loading:\n isLoading ||\n paging ||\n now() < pageLandsAt ||\n messages.length === 0 ||\n (targetId !== null && !element),\n hasOlder: deepAgentHistoryHasOlder(extras),\n pagesLoaded,\n elapsedMs: now() - waitingSince,\n });\n switch (step) {\n case \"reveal\": {\n if (!viewport || !element) return;\n if (!claim()) {\n // Another transcript showing this conversation took the pickup.\n settled = true;\n clearInterval(timer);\n return;\n }\n scrollMessageIntoView({ viewport, element });\n unhighlight = highlightMessage(element);\n revealed = { element, viewport, at: now() };\n const stop = () => {\n personScrolled = true;\n };\n const events = [\"wheel\", \"touchstart\", \"pointerdown\", \"keydown\"] as const;\n for (const event of events) viewport.addEventListener(event, stop, { passive: true });\n detach = () => {\n for (const event of events) viewport.removeEventListener(event, stop);\n };\n return;\n }\n case \"page\":\n pageOlder();\n return;\n case \"wait\":\n return;\n case \"bottom\":\n settle();\n return;\n }\n };\n const timer = setInterval(tick, HANDOFF_CURSOR_POLL_MS);\n tick();\n return ({ endHold = false } = {}) => {\n if (endHold) unhighlight?.();\n if (settled) return;\n // A revealed message's hold finishes on its own timer, unless a newer pickup took over.\n if (revealed && !endHold) return;\n settled = true;\n clearInterval(timer);\n detach?.();\n unhighlight?.();\n };\n}\n\n/**\n * Opens `threadId`'s transcript at the message the source was reading, once,\n * when this surface picked the conversation up with a cursor\n * (`recordHandoffArrival({ cursor })`). When two transcripts show the\n * conversation, the first to find the message owns the pickup. Renders nothing.\n */\nexport const HandoffCursorReveal: FC<{ threadId: string | null | undefined }> = ({ threadId }) => {\n const cursor = useHandoffArrivalCursor(threadId);\n const aui = useAui();\n const viewport = useThreadViewport({\n optional: true,\n selector: (state: ThreadViewportState) => state.element.viewport,\n });\n const viewportRef = useRef(viewport);\n viewportRef.current = viewport;\n // The latest pickup's reveal: its hold outlives the claim that settles its cursor.\n const latestRef = useRef<ReturnType<typeof startHandoffCursorReveal> | null>(null);\n useEffect(() => {\n if (!threadId || !cursor) return undefined;\n // A newer pickup takes over: the previous message stops being held and highlighted.\n latestRef.current?.({ endHold: true });\n const cancel = startHandoffCursorReveal({\n cursor,\n readThread: () => {\n const state = aui.thread().getState();\n return { messages: state.messages, isLoading: state.isLoading, extras: state.extras };\n },\n readViewport: () => viewportRef.current ?? null,\n // Settled before the scroll, so a second transcript's reveal stops at its claim.\n claim: () => {\n if (readHandoffArrival(threadId)?.cursor !== cursor) return false;\n settleHandoffArrivalCursor(threadId);\n return true;\n },\n // Only this pickup's: a revealed message's hold ends after its claim\n // settled the cursor, when a later handover may have recorded another.\n onSettled: () => {\n if (readHandoffArrival(threadId)?.cursor === cursor) settleHandoffArrivalCursor(threadId);\n },\n });\n latestRef.current = cancel;\n return cancel;\n }, [aui, cursor, threadId]);\n return null;\n};\n"],"names":["THREAD_VIEWPORT_ATTR","THREAD_VIEWPORT_FOOTER_ATTR","readingCursorOf","useThreadViewportStore","useCallback","HANDOFF_CURSOR_HIGHLIGHT_MS","loadOlderDeepAgentHistoryPage","readDeepAgentStatewireExtras","isHistoryPageLostToReconnect","HANDOFF_CURSOR_SETTLE_MS","handoffCursorMessageId","MESSAGE_ROW_ID_ATTRIBUTE","handoffCursorRevealStep","deepAgentHistoryHasOlder","useHandoffArrivalCursor","useAui","useThreadViewport","useRef","useEffect","readHandoffArrival","settleHandoffArrivalCursor"],"mappings":"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"}
@@ -3,7 +3,7 @@ import { useRef, useEffect, useCallback } from "react";
3
3
  import { THREAD_VIEWPORT_ATTR, THREAD_VIEWPORT_FOOTER_ATTR } from "./statewire-history-edge.js";
4
4
  import { useHandoffArrivalCursor, readHandoffArrival, settleHandoffArrivalCursor } from "../../agent-runtime/src/handoff-arrival.js";
5
5
  import { readingCursorOf, handoffCursorMessageId, MESSAGE_ROW_ID_ATTRIBUTE, handoffCursorRevealStep, HANDOFF_CURSOR_HIGHLIGHT_MS, HANDOFF_CURSOR_SETTLE_MS } from "../../agent-runtime/src/handoff-cursor.js";
6
- import { deepAgentHistoryHasOlder, loadOlderDeepAgentHistoryPage, readDeepAgentStatewireExtras } from "../../agent-runtime/src/edit-command.js";
6
+ import { deepAgentHistoryHasOlder, loadOlderDeepAgentHistoryPage, isHistoryPageLostToReconnect, readDeepAgentStatewireExtras } from "../../agent-runtime/src/edit-command.js";
7
7
  const THREAD_ROOT_SELECTOR = "[data-aui-thread-root]";
8
8
  const HANDOFF_CURSOR_REVEALED_ATTR = "data-handoff-cursor-revealed";
9
9
  const HANDOFF_CURSOR_POLL_MS = 150;
@@ -115,8 +115,8 @@ function startHandoffCursorReveal({
115
115
  }
116
116
  pagesLoaded += 1;
117
117
  waitingSince = now();
118
- }).catch(() => {
119
- pagesLoaded = Number.POSITIVE_INFINITY;
118
+ }).catch((error) => {
119
+ if (!isHistoryPageLostToReconnect(error)) pagesLoaded = Number.POSITIVE_INFINITY;
120
120
  }).finally(() => {
121
121
  paging = false;
122
122
  pageLandsAt = now() + 2 * HANDOFF_CURSOR_POLL_MS;