@athenaintel/react 0.23.2 → 0.23.3

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 (87) hide show
  1. package/README.md +40 -1
  2. package/dist/chat/AthenaChat.cjs +129 -29
  3. package/dist/chat/AthenaChat.cjs.map +1 -1
  4. package/dist/chat/AthenaChat.d.ts +32 -1
  5. package/dist/chat/AthenaChat.js +130 -30
  6. package/dist/chat/AthenaChat.js.map +1 -1
  7. package/dist/chat/athena-composer-controls.js +1 -1
  8. package/dist/chat/statewire-client-tools.cjs +20 -0
  9. package/dist/chat/statewire-client-tools.cjs.map +1 -1
  10. package/dist/chat/statewire-client-tools.d.ts +27 -0
  11. package/dist/chat/statewire-client-tools.js +20 -0
  12. package/dist/chat/statewire-client-tools.js.map +1 -1
  13. package/dist/chat/use-athena-run-live.cjs.map +1 -1
  14. package/dist/chat/use-athena-run-live.d.ts +7 -0
  15. package/dist/chat/use-athena-run-live.js.map +1 -1
  16. package/dist/chat-ui/composer/tiptap-composer.cjs +16 -1
  17. package/dist/chat-ui/composer/tiptap-composer.cjs.map +1 -1
  18. package/dist/chat-ui/composer/tiptap-composer.js +16 -1
  19. package/dist/chat-ui/composer/tiptap-composer.js.map +1 -1
  20. package/dist/composer/composer-handoff.cjs +121 -0
  21. package/dist/composer/composer-handoff.cjs.map +1 -0
  22. package/dist/composer/composer-handoff.d.ts +89 -0
  23. package/dist/composer/composer-handoff.js +121 -0
  24. package/dist/composer/composer-handoff.js.map +1 -0
  25. package/dist/composer/index.d.ts +2 -0
  26. package/dist/composer/useAppendToComposer.cjs +19 -7
  27. package/dist/composer/useAppendToComposer.cjs.map +1 -1
  28. package/dist/composer/useAppendToComposer.d.ts +22 -4
  29. package/dist/composer/useAppendToComposer.js +20 -8
  30. package/dist/composer/useAppendToComposer.js.map +1 -1
  31. package/dist/composer/useSendMessage.cjs +26 -8
  32. package/dist/composer/useSendMessage.cjs.map +1 -1
  33. package/dist/composer/useSendMessage.d.ts +8 -0
  34. package/dist/composer/useSendMessage.js +27 -9
  35. package/dist/composer/useSendMessage.js.map +1 -1
  36. package/dist/diagnostics/athena-diagnostics.cjs.map +1 -1
  37. package/dist/diagnostics/athena-diagnostics.d.ts +1 -1
  38. package/dist/diagnostics/athena-diagnostics.js.map +1 -1
  39. package/dist/diagnostics/errors.cjs +2 -0
  40. package/dist/diagnostics/errors.cjs.map +1 -1
  41. package/dist/diagnostics/errors.d.ts +2 -0
  42. package/dist/diagnostics/errors.js +2 -0
  43. package/dist/diagnostics/errors.js.map +1 -1
  44. package/dist/dictation/DictationStrip.cjs +60 -0
  45. package/dist/dictation/DictationStrip.cjs.map +1 -0
  46. package/dist/dictation/DictationStrip.d.ts +25 -0
  47. package/dist/dictation/DictationStrip.js +60 -0
  48. package/dist/dictation/DictationStrip.js.map +1 -0
  49. package/dist/dictation/audio-level-history.cjs +68 -0
  50. package/dist/dictation/audio-level-history.cjs.map +1 -0
  51. package/dist/dictation/audio-level-history.d.ts +27 -0
  52. package/dist/dictation/audio-level-history.js +68 -0
  53. package/dist/dictation/audio-level-history.js.map +1 -0
  54. package/dist/dictation/dictation-controls.cjs +60 -0
  55. package/dist/dictation/dictation-controls.cjs.map +1 -0
  56. package/dist/dictation/dictation-controls.d.ts +15 -0
  57. package/dist/dictation/dictation-controls.js +60 -0
  58. package/dist/dictation/dictation-controls.js.map +1 -0
  59. package/dist/dictation/dictation-engine.cjs +128 -0
  60. package/dist/dictation/dictation-engine.cjs.map +1 -0
  61. package/dist/dictation/dictation-engine.d.ts +87 -0
  62. package/dist/dictation/dictation-engine.js +128 -0
  63. package/dist/dictation/dictation-engine.js.map +1 -0
  64. package/dist/dictation/dictation-state.cjs +68 -0
  65. package/dist/dictation/dictation-state.cjs.map +1 -0
  66. package/dist/dictation/dictation-state.d.ts +58 -0
  67. package/dist/dictation/dictation-state.js +68 -0
  68. package/dist/dictation/dictation-state.js.map +1 -0
  69. package/dist/dictation/use-dictation.cjs +204 -0
  70. package/dist/dictation/use-dictation.cjs.map +1 -0
  71. package/dist/dictation/use-dictation.d.ts +47 -0
  72. package/dist/dictation/use-dictation.js +204 -0
  73. package/dist/dictation/use-dictation.js.map +1 -0
  74. package/dist/index.cjs +13 -0
  75. package/dist/index.cjs.map +1 -1
  76. package/dist/index.d.ts +11 -0
  77. package/dist/index.js +13 -0
  78. package/dist/index.js.map +1 -1
  79. package/dist/packages/react/package.json.cjs +1 -1
  80. package/dist/packages/react/package.json.js +1 -1
  81. package/dist/provider/AthenaProvider.cjs +42 -3
  82. package/dist/provider/AthenaProvider.cjs.map +1 -1
  83. package/dist/provider/AthenaProvider.d.ts +1 -1
  84. package/dist/provider/AthenaProvider.js +43 -4
  85. package/dist/provider/AthenaProvider.js.map +1 -1
  86. package/dist/styles.css +1 -1
  87. package/package.json +1 -1
@@ -70,6 +70,26 @@ function collectStatewireClientTools(toolkit) {
70
70
  function statewireClientToolWireEntries(tools) {
71
71
  return clientTools.clientToolRunConfigEntries(tools);
72
72
  }
73
+ function backendDeclaredFrontendToolIds(toolkit) {
74
+ return Object.entries(toolkit).filter(([, entry]) => !hasCallableExecute(entry)).map(([name]) => name);
75
+ }
76
+ const CLIENT_TOOL_SURFACE_APP_IDS = {
77
+ olympus: "olympus",
78
+ officeAddin: "athena-office-addin",
79
+ mobile: "athena-mobile"
80
+ };
81
+ const SDK_APP_ID_PATTERN = /^computer_asset_[0-9a-f-]{8,64}$/;
82
+ function resolveClientToolSurface(appId) {
83
+ if (typeof appId !== "string" || appId.length === 0) return null;
84
+ if (appId === CLIENT_TOOL_SURFACE_APP_IDS.olympus) return "web";
85
+ if (appId === CLIENT_TOOL_SURFACE_APP_IDS.officeAddin) return "office";
86
+ if (appId === CLIENT_TOOL_SURFACE_APP_IDS.mobile) return "mobile";
87
+ if (SDK_APP_ID_PATTERN.test(appId)) return "sdk";
88
+ return null;
89
+ }
90
+ exports.CLIENT_TOOL_SURFACE_APP_IDS = CLIENT_TOOL_SURFACE_APP_IDS;
91
+ exports.backendDeclaredFrontendToolIds = backendDeclaredFrontendToolIds;
73
92
  exports.collectStatewireClientTools = collectStatewireClientTools;
93
+ exports.resolveClientToolSurface = resolveClientToolSurface;
74
94
  exports.statewireClientToolWireEntries = statewireClientToolWireEntries;
75
95
  //# sourceMappingURL=statewire-client-tools.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"statewire-client-tools.cjs","sources":["../../src/chat/statewire-client-tools.ts"],"sourcesContent":["import { athenaDiagnostics } from '../diagnostics/athena-diagnostics';\nimport type { Toolkit } from '@assistant-ui/react';\nimport {\n type ClientToolHandlerContext,\n clientToolRunConfigEntries,\n type RegisteredClientTool,\n} from '@athenaintel/agent-runtime';\nimport { ToolResponse, toJSONSchema } from 'assistant-stream';\n\n/**\n * @deprecated The SDK's client tools ARE the shared `RegisteredClientTool`\n * shape from `@athenaintel/agent-runtime` (`definition` + `handler`); import\n * that type instead. Retained for one release so existing imports type-check.\n */\nexport type StatewireClientTool = RegisteredClientTool;\n\nfunction hasCallableExecute(\n entry: unknown,\n): entry is { execute: (args: Record<string, unknown>, context: unknown) => unknown } {\n return (\n !!entry &&\n typeof entry === 'object' &&\n typeof (entry as { execute?: unknown }).execute === 'function'\n );\n}\n\n/**\n * Collect the client-executable tools from a frontend toolkit, in the shared\n * `RegisteredClientTool` shape every statewire surface declares and executes.\n *\n * The deep-agent runtime drops frontend (`ui_*`) tool definitions, so tools\n * with a local `execute` must instead be declared through\n * `runConfig.custom.client_tools` and answered by the statewire client-tool\n * bridge. Schema conversion reuses `toJSONSchema` — the same conversion\n * primitive as the legacy assistant-transport — and a tool whose schema cannot\n * be converted is skipped with a warning rather than failing the whole set.\n * SDK tools never gate on write consent (`requiresWrite: false`): a toolkit\n * `execute` is the host app's own code.\n */\nexport function collectStatewireClientTools(toolkit: Toolkit): RegisteredClientTool[] {\n const executable = Object.entries(toolkit).filter(([, entry]) => hasCallableExecute(entry));\n if (executable.length === 0) return [];\n\n return executable.flatMap(([name, entry]) => {\n if (entry.parameters === undefined) return [];\n\n let parameters: Record<string, unknown>;\n try {\n parameters = { ...toJSONSchema(entry.parameters) };\n } catch (error) {\n athenaDiagnostics.emit('tools.schema.warning', {\n level: 'warn',\n data: {\n tool: name,\n message: 'Frontend tool schema could not be converted for statewire; the tool is not registered',\n error: error instanceof Error ? error.message : String(error),\n },\n });\n return [];\n }\n\n const execute = (entry as {\n execute: (args: Record<string, unknown>, context: unknown) => unknown;\n }).execute;\n return [\n {\n definition: {\n name,\n description: entry.description ?? '',\n parameters,\n requiresWrite: false,\n },\n handler: async (args, context?: ClientToolHandlerContext) => {\n const controller = new AbortController();\n const output = await execute(args, {\n // The bridge always supplies the parked call's id; a host invoking\n // the handler directly has no call to correlate.\n toolCallId: context?.toolCallId ?? '',\n abortSignal: controller.signal,\n human: () =>\n Promise.reject(\n new Error('Human input is not supported for statewire client tools.'),\n ),\n });\n return output instanceof ToolResponse ? output.result : output;\n },\n },\n ];\n });\n}\n\n/** Wire entries for `runConfig.custom.client_tools` — the shared contract from\n * `@athenaintel/agent-runtime`, so the SDK cannot drift from the other surfaces. */\nexport function statewireClientToolWireEntries(\n tools: readonly RegisteredClientTool[],\n): Record<string, unknown>[] {\n return clientToolRunConfigEntries(tools);\n}\n"],"names":["toJSONSchema","athenaDiagnostics","ToolResponse","clientToolRunConfigEntries"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAgBA,SAAS,mBACP,OACoF;AACpF,SACE,CAAC,CAAC,SACF,OAAO,UAAU,YACjB,OAAQ,MAAgC,YAAY;AAExD;AAeO,SAAS,4BAA4B,SAA0C;AACpF,QAAM,aAAa,OAAO,QAAQ,OAAO,EAAE,OAAO,CAAC,GAAG,KAAK,MAAM,mBAAmB,KAAK,CAAC;AAC1F,MAAI,WAAW,WAAW,EAAG,QAAO,CAAA;AAEpC,SAAO,WAAW,QAAQ,CAAC,CAAC,MAAM,KAAK,MAAM;AAC3C,QAAI,MAAM,eAAe,OAAW,QAAO,CAAA;AAE3C,QAAI;AACJ,QAAI;AACF,mBAAa,EAAE,GAAGA,gBAAAA,aAAa,MAAM,UAAU,EAAA;AAAA,IACjD,SAAS,OAAO;AACdC,wBAAAA,kBAAkB,KAAK,wBAAwB;AAAA,QAC7C,OAAO;AAAA,QACP,MAAM;AAAA,UACJ,MAAM;AAAA,UACN,SAAS;AAAA,UACT,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,QAAA;AAAA,MAC9D,CACD;AACD,aAAO,CAAA;AAAA,IACT;AAEA,UAAM,UAAW,MAEd;AACH,WAAO;AAAA,MACL;AAAA,QACE,YAAY;AAAA,UACV;AAAA,UACA,aAAa,MAAM,eAAe;AAAA,UAClC;AAAA,UACA,eAAe;AAAA,QAAA;AAAA,QAEjB,SAAS,OAAO,MAAM,YAAuC;AAC3D,gBAAM,aAAa,IAAI,gBAAA;AACvB,gBAAM,SAAS,MAAM,QAAQ,MAAM;AAAA;AAAA;AAAA,YAGjC,aAAY,mCAAS,eAAc;AAAA,YACnC,aAAa,WAAW;AAAA,YACxB,OAAO,MACL,QAAQ;AAAA,cACN,IAAI,MAAM,0DAA0D;AAAA,YAAA;AAAA,UACtE,CACH;AACD,iBAAO,kBAAkBC,gBAAAA,eAAe,OAAO,SAAS;AAAA,QAC1D;AAAA,MAAA;AAAA,IACF;AAAA,EAEJ,CAAC;AACH;AAIO,SAAS,+BACd,OAC2B;AAC3B,SAAOC,YAAAA,2BAA2B,KAAK;AACzC;;;"}
1
+ {"version":3,"file":"statewire-client-tools.cjs","sources":["../../src/chat/statewire-client-tools.ts"],"sourcesContent":["import { athenaDiagnostics } from '../diagnostics/athena-diagnostics';\nimport type { Toolkit } from '@assistant-ui/react';\nimport {\n type ClientToolHandlerContext,\n clientToolRunConfigEntries,\n type RegisteredClientTool,\n} from '@athenaintel/agent-runtime';\nimport { ToolResponse, toJSONSchema } from 'assistant-stream';\n\n/**\n * @deprecated The SDK's client tools ARE the shared `RegisteredClientTool`\n * shape from `@athenaintel/agent-runtime` (`definition` + `handler`); import\n * that type instead. Retained for one release so existing imports type-check.\n */\nexport type StatewireClientTool = RegisteredClientTool;\n\nfunction hasCallableExecute(\n entry: unknown,\n): entry is { execute: (args: Record<string, unknown>, context: unknown) => unknown } {\n return (\n !!entry &&\n typeof entry === 'object' &&\n typeof (entry as { execute?: unknown }).execute === 'function'\n );\n}\n\n/**\n * Collect the client-executable tools from a frontend toolkit, in the shared\n * `RegisteredClientTool` shape every statewire surface declares and executes.\n *\n * The deep-agent runtime drops frontend (`ui_*`) tool definitions, so tools\n * with a local `execute` must instead be declared through\n * `runConfig.custom.client_tools` and answered by the statewire client-tool\n * bridge. Schema conversion reuses `toJSONSchema` — the same conversion\n * primitive as the legacy assistant-transport — and a tool whose schema cannot\n * be converted is skipped with a warning rather than failing the whole set.\n * SDK tools never gate on write consent (`requiresWrite: false`): a toolkit\n * `execute` is the host app's own code.\n */\nexport function collectStatewireClientTools(toolkit: Toolkit): RegisteredClientTool[] {\n const executable = Object.entries(toolkit).filter(([, entry]) => hasCallableExecute(entry));\n if (executable.length === 0) return [];\n\n return executable.flatMap(([name, entry]) => {\n if (entry.parameters === undefined) return [];\n\n let parameters: Record<string, unknown>;\n try {\n parameters = { ...toJSONSchema(entry.parameters) };\n } catch (error) {\n athenaDiagnostics.emit('tools.schema.warning', {\n level: 'warn',\n data: {\n tool: name,\n message: 'Frontend tool schema could not be converted for statewire; the tool is not registered',\n error: error instanceof Error ? error.message : String(error),\n },\n });\n return [];\n }\n\n const execute = (entry as {\n execute: (args: Record<string, unknown>, context: unknown) => unknown;\n }).execute;\n return [\n {\n definition: {\n name,\n description: entry.description ?? '',\n parameters,\n requiresWrite: false,\n },\n handler: async (args, context?: ClientToolHandlerContext) => {\n const controller = new AbortController();\n const output = await execute(args, {\n // The bridge always supplies the parked call's id; a host invoking\n // the handler directly has no call to correlate.\n toolCallId: context?.toolCallId ?? '',\n abortSignal: controller.signal,\n human: () =>\n Promise.reject(\n new Error('Human input is not supported for statewire client tools.'),\n ),\n });\n return output instanceof ToolResponse ? output.result : output;\n },\n },\n ];\n });\n}\n\n/** Wire entries for `runConfig.custom.client_tools` — the shared contract from\n * `@athenaintel/agent-runtime`, so the SDK cannot drift from the other surfaces. */\nexport function statewireClientToolWireEntries(\n tools: readonly RegisteredClientTool[],\n): Record<string, unknown>[] {\n return clientToolRunConfigEntries(tools);\n}\n\n/**\n * The frontend-toolkit entries that must ride `enabled_tools`: declarations\n * WITHOUT a local `execute`, which name registry tools the backend already\n * knows how to bind (Olympus's frontend-executed tool ids).\n *\n * Executable entries are deliberately left out. They are declared through\n * `client_tools`, and the run binds them by name; putting the same name into\n * `enabled_tools` would also switch on any backend toolkit that happens to\n * own a tool of that name, and the backend wins the resulting collision —\n * the client tool is dropped and the model calls the backend one instead.\n */\nexport function backendDeclaredFrontendToolIds(toolkit: Toolkit): string[] {\n return Object.entries(toolkit)\n .filter(([, entry]) => !hasCallableExecute(entry))\n .map(([name]) => name);\n}\n\n/**\n * The `app_id` values the deep-agent runtime accepts as a client-tool surface.\n * Mirrors `agora/agora/graphs/athena_deep_agent/client_tools.py`: the fixed\n * first-party ids plus the `computer_asset_<uuid>` shape Athena's provisioning\n * injects into a computer-asset app. Any other `app_id` (or none) gets no\n * surface, and the runtime drops every `client_tools` declaration for the run.\n */\nexport const CLIENT_TOOL_SURFACE_APP_IDS = {\n olympus: 'olympus',\n officeAddin: 'athena-office-addin',\n mobile: 'athena-mobile',\n} as const;\n\nconst SDK_APP_ID_PATTERN = /^computer_asset_[0-9a-f-]{8,64}$/;\n\nexport type ClientToolSurface = 'web' | 'office' | 'mobile' | 'sdk';\n\n/** The surface an `app_id` selects, or null when the runtime would refuse it. */\nexport function resolveClientToolSurface(appId: string | undefined | null): ClientToolSurface | null {\n if (typeof appId !== 'string' || appId.length === 0) return null;\n if (appId === CLIENT_TOOL_SURFACE_APP_IDS.olympus) return 'web';\n if (appId === CLIENT_TOOL_SURFACE_APP_IDS.officeAddin) return 'office';\n if (appId === CLIENT_TOOL_SURFACE_APP_IDS.mobile) return 'mobile';\n if (SDK_APP_ID_PATTERN.test(appId)) return 'sdk';\n return null;\n}\n"],"names":["toJSONSchema","athenaDiagnostics","ToolResponse","clientToolRunConfigEntries"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAgBA,SAAS,mBACP,OACoF;AACpF,SACE,CAAC,CAAC,SACF,OAAO,UAAU,YACjB,OAAQ,MAAgC,YAAY;AAExD;AAeO,SAAS,4BAA4B,SAA0C;AACpF,QAAM,aAAa,OAAO,QAAQ,OAAO,EAAE,OAAO,CAAC,GAAG,KAAK,MAAM,mBAAmB,KAAK,CAAC;AAC1F,MAAI,WAAW,WAAW,EAAG,QAAO,CAAA;AAEpC,SAAO,WAAW,QAAQ,CAAC,CAAC,MAAM,KAAK,MAAM;AAC3C,QAAI,MAAM,eAAe,OAAW,QAAO,CAAA;AAE3C,QAAI;AACJ,QAAI;AACF,mBAAa,EAAE,GAAGA,gBAAAA,aAAa,MAAM,UAAU,EAAA;AAAA,IACjD,SAAS,OAAO;AACdC,wBAAAA,kBAAkB,KAAK,wBAAwB;AAAA,QAC7C,OAAO;AAAA,QACP,MAAM;AAAA,UACJ,MAAM;AAAA,UACN,SAAS;AAAA,UACT,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,QAAA;AAAA,MAC9D,CACD;AACD,aAAO,CAAA;AAAA,IACT;AAEA,UAAM,UAAW,MAEd;AACH,WAAO;AAAA,MACL;AAAA,QACE,YAAY;AAAA,UACV;AAAA,UACA,aAAa,MAAM,eAAe;AAAA,UAClC;AAAA,UACA,eAAe;AAAA,QAAA;AAAA,QAEjB,SAAS,OAAO,MAAM,YAAuC;AAC3D,gBAAM,aAAa,IAAI,gBAAA;AACvB,gBAAM,SAAS,MAAM,QAAQ,MAAM;AAAA;AAAA;AAAA,YAGjC,aAAY,mCAAS,eAAc;AAAA,YACnC,aAAa,WAAW;AAAA,YACxB,OAAO,MACL,QAAQ;AAAA,cACN,IAAI,MAAM,0DAA0D;AAAA,YAAA;AAAA,UACtE,CACH;AACD,iBAAO,kBAAkBC,gBAAAA,eAAe,OAAO,SAAS;AAAA,QAC1D;AAAA,MAAA;AAAA,IACF;AAAA,EAEJ,CAAC;AACH;AAIO,SAAS,+BACd,OAC2B;AAC3B,SAAOC,YAAAA,2BAA2B,KAAK;AACzC;AAaO,SAAS,+BAA+B,SAA4B;AACzE,SAAO,OAAO,QAAQ,OAAO,EAC1B,OAAO,CAAC,CAAA,EAAG,KAAK,MAAM,CAAC,mBAAmB,KAAK,CAAC,EAChD,IAAI,CAAC,CAAC,IAAI,MAAM,IAAI;AACzB;AASO,MAAM,8BAA8B;AAAA,EACzC,SAAS;AAAA,EACT,aAAa;AAAA,EACb,QAAQ;AACV;AAEA,MAAM,qBAAqB;AAKpB,SAAS,yBAAyB,OAA4D;AACnG,MAAI,OAAO,UAAU,YAAY,MAAM,WAAW,EAAG,QAAO;AAC5D,MAAI,UAAU,4BAA4B,QAAS,QAAO;AAC1D,MAAI,UAAU,4BAA4B,YAAa,QAAO;AAC9D,MAAI,UAAU,4BAA4B,OAAQ,QAAO;AACzD,MAAI,mBAAmB,KAAK,KAAK,EAAG,QAAO;AAC3C,SAAO;AACT;;;;;;"}
@@ -23,3 +23,30 @@ export declare function collectStatewireClientTools(toolkit: Toolkit): Registere
23
23
  /** Wire entries for `runConfig.custom.client_tools` — the shared contract from
24
24
  * `@athenaintel/agent-runtime`, so the SDK cannot drift from the other surfaces. */
25
25
  export declare function statewireClientToolWireEntries(tools: readonly RegisteredClientTool[]): Record<string, unknown>[];
26
+ /**
27
+ * The frontend-toolkit entries that must ride `enabled_tools`: declarations
28
+ * WITHOUT a local `execute`, which name registry tools the backend already
29
+ * knows how to bind (Olympus's frontend-executed tool ids).
30
+ *
31
+ * Executable entries are deliberately left out. They are declared through
32
+ * `client_tools`, and the run binds them by name; putting the same name into
33
+ * `enabled_tools` would also switch on any backend toolkit that happens to
34
+ * own a tool of that name, and the backend wins the resulting collision —
35
+ * the client tool is dropped and the model calls the backend one instead.
36
+ */
37
+ export declare function backendDeclaredFrontendToolIds(toolkit: Toolkit): string[];
38
+ /**
39
+ * The `app_id` values the deep-agent runtime accepts as a client-tool surface.
40
+ * Mirrors `agora/agora/graphs/athena_deep_agent/client_tools.py`: the fixed
41
+ * first-party ids plus the `computer_asset_<uuid>` shape Athena's provisioning
42
+ * injects into a computer-asset app. Any other `app_id` (or none) gets no
43
+ * surface, and the runtime drops every `client_tools` declaration for the run.
44
+ */
45
+ export declare const CLIENT_TOOL_SURFACE_APP_IDS: {
46
+ readonly olympus: 'olympus';
47
+ readonly officeAddin: 'athena-office-addin';
48
+ readonly mobile: 'athena-mobile';
49
+ };
50
+ export type ClientToolSurface = 'web' | 'office' | 'mobile' | 'sdk';
51
+ /** The surface an `app_id` selects, or null when the runtime would refuse it. */
52
+ export declare function resolveClientToolSurface(appId: string | undefined | null): ClientToolSurface | null;
@@ -68,8 +68,28 @@ function collectStatewireClientTools(toolkit) {
68
68
  function statewireClientToolWireEntries(tools) {
69
69
  return clientToolRunConfigEntries(tools);
70
70
  }
71
+ function backendDeclaredFrontendToolIds(toolkit) {
72
+ return Object.entries(toolkit).filter(([, entry]) => !hasCallableExecute(entry)).map(([name]) => name);
73
+ }
74
+ const CLIENT_TOOL_SURFACE_APP_IDS = {
75
+ olympus: "olympus",
76
+ officeAddin: "athena-office-addin",
77
+ mobile: "athena-mobile"
78
+ };
79
+ const SDK_APP_ID_PATTERN = /^computer_asset_[0-9a-f-]{8,64}$/;
80
+ function resolveClientToolSurface(appId) {
81
+ if (typeof appId !== "string" || appId.length === 0) return null;
82
+ if (appId === CLIENT_TOOL_SURFACE_APP_IDS.olympus) return "web";
83
+ if (appId === CLIENT_TOOL_SURFACE_APP_IDS.officeAddin) return "office";
84
+ if (appId === CLIENT_TOOL_SURFACE_APP_IDS.mobile) return "mobile";
85
+ if (SDK_APP_ID_PATTERN.test(appId)) return "sdk";
86
+ return null;
87
+ }
71
88
  export {
89
+ CLIENT_TOOL_SURFACE_APP_IDS,
90
+ backendDeclaredFrontendToolIds,
72
91
  collectStatewireClientTools,
92
+ resolveClientToolSurface,
73
93
  statewireClientToolWireEntries
74
94
  };
75
95
  //# sourceMappingURL=statewire-client-tools.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"statewire-client-tools.js","sources":["../../src/chat/statewire-client-tools.ts"],"sourcesContent":["import { athenaDiagnostics } from '../diagnostics/athena-diagnostics';\nimport type { Toolkit } from '@assistant-ui/react';\nimport {\n type ClientToolHandlerContext,\n clientToolRunConfigEntries,\n type RegisteredClientTool,\n} from '@athenaintel/agent-runtime';\nimport { ToolResponse, toJSONSchema } from 'assistant-stream';\n\n/**\n * @deprecated The SDK's client tools ARE the shared `RegisteredClientTool`\n * shape from `@athenaintel/agent-runtime` (`definition` + `handler`); import\n * that type instead. Retained for one release so existing imports type-check.\n */\nexport type StatewireClientTool = RegisteredClientTool;\n\nfunction hasCallableExecute(\n entry: unknown,\n): entry is { execute: (args: Record<string, unknown>, context: unknown) => unknown } {\n return (\n !!entry &&\n typeof entry === 'object' &&\n typeof (entry as { execute?: unknown }).execute === 'function'\n );\n}\n\n/**\n * Collect the client-executable tools from a frontend toolkit, in the shared\n * `RegisteredClientTool` shape every statewire surface declares and executes.\n *\n * The deep-agent runtime drops frontend (`ui_*`) tool definitions, so tools\n * with a local `execute` must instead be declared through\n * `runConfig.custom.client_tools` and answered by the statewire client-tool\n * bridge. Schema conversion reuses `toJSONSchema` — the same conversion\n * primitive as the legacy assistant-transport — and a tool whose schema cannot\n * be converted is skipped with a warning rather than failing the whole set.\n * SDK tools never gate on write consent (`requiresWrite: false`): a toolkit\n * `execute` is the host app's own code.\n */\nexport function collectStatewireClientTools(toolkit: Toolkit): RegisteredClientTool[] {\n const executable = Object.entries(toolkit).filter(([, entry]) => hasCallableExecute(entry));\n if (executable.length === 0) return [];\n\n return executable.flatMap(([name, entry]) => {\n if (entry.parameters === undefined) return [];\n\n let parameters: Record<string, unknown>;\n try {\n parameters = { ...toJSONSchema(entry.parameters) };\n } catch (error) {\n athenaDiagnostics.emit('tools.schema.warning', {\n level: 'warn',\n data: {\n tool: name,\n message: 'Frontend tool schema could not be converted for statewire; the tool is not registered',\n error: error instanceof Error ? error.message : String(error),\n },\n });\n return [];\n }\n\n const execute = (entry as {\n execute: (args: Record<string, unknown>, context: unknown) => unknown;\n }).execute;\n return [\n {\n definition: {\n name,\n description: entry.description ?? '',\n parameters,\n requiresWrite: false,\n },\n handler: async (args, context?: ClientToolHandlerContext) => {\n const controller = new AbortController();\n const output = await execute(args, {\n // The bridge always supplies the parked call's id; a host invoking\n // the handler directly has no call to correlate.\n toolCallId: context?.toolCallId ?? '',\n abortSignal: controller.signal,\n human: () =>\n Promise.reject(\n new Error('Human input is not supported for statewire client tools.'),\n ),\n });\n return output instanceof ToolResponse ? output.result : output;\n },\n },\n ];\n });\n}\n\n/** Wire entries for `runConfig.custom.client_tools` — the shared contract from\n * `@athenaintel/agent-runtime`, so the SDK cannot drift from the other surfaces. */\nexport function statewireClientToolWireEntries(\n tools: readonly RegisteredClientTool[],\n): Record<string, unknown>[] {\n return clientToolRunConfigEntries(tools);\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;AAgBA,SAAS,mBACP,OACoF;AACpF,SACE,CAAC,CAAC,SACF,OAAO,UAAU,YACjB,OAAQ,MAAgC,YAAY;AAExD;AAeO,SAAS,4BAA4B,SAA0C;AACpF,QAAM,aAAa,OAAO,QAAQ,OAAO,EAAE,OAAO,CAAC,GAAG,KAAK,MAAM,mBAAmB,KAAK,CAAC;AAC1F,MAAI,WAAW,WAAW,EAAG,QAAO,CAAA;AAEpC,SAAO,WAAW,QAAQ,CAAC,CAAC,MAAM,KAAK,MAAM;AAC3C,QAAI,MAAM,eAAe,OAAW,QAAO,CAAA;AAE3C,QAAI;AACJ,QAAI;AACF,mBAAa,EAAE,GAAG,aAAa,MAAM,UAAU,EAAA;AAAA,IACjD,SAAS,OAAO;AACd,wBAAkB,KAAK,wBAAwB;AAAA,QAC7C,OAAO;AAAA,QACP,MAAM;AAAA,UACJ,MAAM;AAAA,UACN,SAAS;AAAA,UACT,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,QAAA;AAAA,MAC9D,CACD;AACD,aAAO,CAAA;AAAA,IACT;AAEA,UAAM,UAAW,MAEd;AACH,WAAO;AAAA,MACL;AAAA,QACE,YAAY;AAAA,UACV;AAAA,UACA,aAAa,MAAM,eAAe;AAAA,UAClC;AAAA,UACA,eAAe;AAAA,QAAA;AAAA,QAEjB,SAAS,OAAO,MAAM,YAAuC;AAC3D,gBAAM,aAAa,IAAI,gBAAA;AACvB,gBAAM,SAAS,MAAM,QAAQ,MAAM;AAAA;AAAA;AAAA,YAGjC,aAAY,mCAAS,eAAc;AAAA,YACnC,aAAa,WAAW;AAAA,YACxB,OAAO,MACL,QAAQ;AAAA,cACN,IAAI,MAAM,0DAA0D;AAAA,YAAA;AAAA,UACtE,CACH;AACD,iBAAO,kBAAkB,eAAe,OAAO,SAAS;AAAA,QAC1D;AAAA,MAAA;AAAA,IACF;AAAA,EAEJ,CAAC;AACH;AAIO,SAAS,+BACd,OAC2B;AAC3B,SAAO,2BAA2B,KAAK;AACzC;"}
1
+ {"version":3,"file":"statewire-client-tools.js","sources":["../../src/chat/statewire-client-tools.ts"],"sourcesContent":["import { athenaDiagnostics } from '../diagnostics/athena-diagnostics';\nimport type { Toolkit } from '@assistant-ui/react';\nimport {\n type ClientToolHandlerContext,\n clientToolRunConfigEntries,\n type RegisteredClientTool,\n} from '@athenaintel/agent-runtime';\nimport { ToolResponse, toJSONSchema } from 'assistant-stream';\n\n/**\n * @deprecated The SDK's client tools ARE the shared `RegisteredClientTool`\n * shape from `@athenaintel/agent-runtime` (`definition` + `handler`); import\n * that type instead. Retained for one release so existing imports type-check.\n */\nexport type StatewireClientTool = RegisteredClientTool;\n\nfunction hasCallableExecute(\n entry: unknown,\n): entry is { execute: (args: Record<string, unknown>, context: unknown) => unknown } {\n return (\n !!entry &&\n typeof entry === 'object' &&\n typeof (entry as { execute?: unknown }).execute === 'function'\n );\n}\n\n/**\n * Collect the client-executable tools from a frontend toolkit, in the shared\n * `RegisteredClientTool` shape every statewire surface declares and executes.\n *\n * The deep-agent runtime drops frontend (`ui_*`) tool definitions, so tools\n * with a local `execute` must instead be declared through\n * `runConfig.custom.client_tools` and answered by the statewire client-tool\n * bridge. Schema conversion reuses `toJSONSchema` — the same conversion\n * primitive as the legacy assistant-transport — and a tool whose schema cannot\n * be converted is skipped with a warning rather than failing the whole set.\n * SDK tools never gate on write consent (`requiresWrite: false`): a toolkit\n * `execute` is the host app's own code.\n */\nexport function collectStatewireClientTools(toolkit: Toolkit): RegisteredClientTool[] {\n const executable = Object.entries(toolkit).filter(([, entry]) => hasCallableExecute(entry));\n if (executable.length === 0) return [];\n\n return executable.flatMap(([name, entry]) => {\n if (entry.parameters === undefined) return [];\n\n let parameters: Record<string, unknown>;\n try {\n parameters = { ...toJSONSchema(entry.parameters) };\n } catch (error) {\n athenaDiagnostics.emit('tools.schema.warning', {\n level: 'warn',\n data: {\n tool: name,\n message: 'Frontend tool schema could not be converted for statewire; the tool is not registered',\n error: error instanceof Error ? error.message : String(error),\n },\n });\n return [];\n }\n\n const execute = (entry as {\n execute: (args: Record<string, unknown>, context: unknown) => unknown;\n }).execute;\n return [\n {\n definition: {\n name,\n description: entry.description ?? '',\n parameters,\n requiresWrite: false,\n },\n handler: async (args, context?: ClientToolHandlerContext) => {\n const controller = new AbortController();\n const output = await execute(args, {\n // The bridge always supplies the parked call's id; a host invoking\n // the handler directly has no call to correlate.\n toolCallId: context?.toolCallId ?? '',\n abortSignal: controller.signal,\n human: () =>\n Promise.reject(\n new Error('Human input is not supported for statewire client tools.'),\n ),\n });\n return output instanceof ToolResponse ? output.result : output;\n },\n },\n ];\n });\n}\n\n/** Wire entries for `runConfig.custom.client_tools` — the shared contract from\n * `@athenaintel/agent-runtime`, so the SDK cannot drift from the other surfaces. */\nexport function statewireClientToolWireEntries(\n tools: readonly RegisteredClientTool[],\n): Record<string, unknown>[] {\n return clientToolRunConfigEntries(tools);\n}\n\n/**\n * The frontend-toolkit entries that must ride `enabled_tools`: declarations\n * WITHOUT a local `execute`, which name registry tools the backend already\n * knows how to bind (Olympus's frontend-executed tool ids).\n *\n * Executable entries are deliberately left out. They are declared through\n * `client_tools`, and the run binds them by name; putting the same name into\n * `enabled_tools` would also switch on any backend toolkit that happens to\n * own a tool of that name, and the backend wins the resulting collision —\n * the client tool is dropped and the model calls the backend one instead.\n */\nexport function backendDeclaredFrontendToolIds(toolkit: Toolkit): string[] {\n return Object.entries(toolkit)\n .filter(([, entry]) => !hasCallableExecute(entry))\n .map(([name]) => name);\n}\n\n/**\n * The `app_id` values the deep-agent runtime accepts as a client-tool surface.\n * Mirrors `agora/agora/graphs/athena_deep_agent/client_tools.py`: the fixed\n * first-party ids plus the `computer_asset_<uuid>` shape Athena's provisioning\n * injects into a computer-asset app. Any other `app_id` (or none) gets no\n * surface, and the runtime drops every `client_tools` declaration for the run.\n */\nexport const CLIENT_TOOL_SURFACE_APP_IDS = {\n olympus: 'olympus',\n officeAddin: 'athena-office-addin',\n mobile: 'athena-mobile',\n} as const;\n\nconst SDK_APP_ID_PATTERN = /^computer_asset_[0-9a-f-]{8,64}$/;\n\nexport type ClientToolSurface = 'web' | 'office' | 'mobile' | 'sdk';\n\n/** The surface an `app_id` selects, or null when the runtime would refuse it. */\nexport function resolveClientToolSurface(appId: string | undefined | null): ClientToolSurface | null {\n if (typeof appId !== 'string' || appId.length === 0) return null;\n if (appId === CLIENT_TOOL_SURFACE_APP_IDS.olympus) return 'web';\n if (appId === CLIENT_TOOL_SURFACE_APP_IDS.officeAddin) return 'office';\n if (appId === CLIENT_TOOL_SURFACE_APP_IDS.mobile) return 'mobile';\n if (SDK_APP_ID_PATTERN.test(appId)) return 'sdk';\n return null;\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;AAgBA,SAAS,mBACP,OACoF;AACpF,SACE,CAAC,CAAC,SACF,OAAO,UAAU,YACjB,OAAQ,MAAgC,YAAY;AAExD;AAeO,SAAS,4BAA4B,SAA0C;AACpF,QAAM,aAAa,OAAO,QAAQ,OAAO,EAAE,OAAO,CAAC,GAAG,KAAK,MAAM,mBAAmB,KAAK,CAAC;AAC1F,MAAI,WAAW,WAAW,EAAG,QAAO,CAAA;AAEpC,SAAO,WAAW,QAAQ,CAAC,CAAC,MAAM,KAAK,MAAM;AAC3C,QAAI,MAAM,eAAe,OAAW,QAAO,CAAA;AAE3C,QAAI;AACJ,QAAI;AACF,mBAAa,EAAE,GAAG,aAAa,MAAM,UAAU,EAAA;AAAA,IACjD,SAAS,OAAO;AACd,wBAAkB,KAAK,wBAAwB;AAAA,QAC7C,OAAO;AAAA,QACP,MAAM;AAAA,UACJ,MAAM;AAAA,UACN,SAAS;AAAA,UACT,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,QAAA;AAAA,MAC9D,CACD;AACD,aAAO,CAAA;AAAA,IACT;AAEA,UAAM,UAAW,MAEd;AACH,WAAO;AAAA,MACL;AAAA,QACE,YAAY;AAAA,UACV;AAAA,UACA,aAAa,MAAM,eAAe;AAAA,UAClC;AAAA,UACA,eAAe;AAAA,QAAA;AAAA,QAEjB,SAAS,OAAO,MAAM,YAAuC;AAC3D,gBAAM,aAAa,IAAI,gBAAA;AACvB,gBAAM,SAAS,MAAM,QAAQ,MAAM;AAAA;AAAA;AAAA,YAGjC,aAAY,mCAAS,eAAc;AAAA,YACnC,aAAa,WAAW;AAAA,YACxB,OAAO,MACL,QAAQ;AAAA,cACN,IAAI,MAAM,0DAA0D;AAAA,YAAA;AAAA,UACtE,CACH;AACD,iBAAO,kBAAkB,eAAe,OAAO,SAAS;AAAA,QAC1D;AAAA,MAAA;AAAA,IACF;AAAA,EAEJ,CAAC;AACH;AAIO,SAAS,+BACd,OAC2B;AAC3B,SAAO,2BAA2B,KAAK;AACzC;AAaO,SAAS,+BAA+B,SAA4B;AACzE,SAAO,OAAO,QAAQ,OAAO,EAC1B,OAAO,CAAC,CAAA,EAAG,KAAK,MAAM,CAAC,mBAAmB,KAAK,CAAC,EAChD,IAAI,CAAC,CAAC,IAAI,MAAM,IAAI;AACzB;AASO,MAAM,8BAA8B;AAAA,EACzC,SAAS;AAAA,EACT,aAAa;AAAA,EACb,QAAQ;AACV;AAEA,MAAM,qBAAqB;AAKpB,SAAS,yBAAyB,OAA4D;AACnG,MAAI,OAAO,UAAU,YAAY,MAAM,WAAW,EAAG,QAAO;AAC5D,MAAI,UAAU,4BAA4B,QAAS,QAAO;AAC1D,MAAI,UAAU,4BAA4B,YAAa,QAAO;AAC9D,MAAI,UAAU,4BAA4B,OAAQ,QAAO;AACzD,MAAI,mBAAmB,KAAK,KAAK,EAAG,QAAO;AAC3C,SAAO;AACT;"}
@@ -1 +1 @@
1
- {"version":3,"file":"use-athena-run-live.cjs","sources":["../../src/chat/use-athena-run-live.ts"],"sourcesContent":["import { useAuiState } from '@assistant-ui/react';\nimport { readRunLive, useDeepAgentConnection } from '@athenaintel/agent-runtime';\n\n/**\n * Whether the thread has a live run — `thread.isRunning` folded through the\n * host's channel state, so a run behind a first dial (a new chat's first\n * send, switching back to a thread that was running) still reads as live\n * while a run behind a genuine reconnect or eviction does not. Surfaces\n * that gate on a live run (message edit, the voice card's live state, the\n * approval resume lock) read this instead of `thread.isRunning`, which\n * follows the presented status and blinks off for the attach round-trip.\n */\nexport function useAthenaRunLive(): boolean {\n const connection = useDeepAgentConnection();\n return useAuiState((s) => readRunLive(s.thread, connection));\n}\n"],"names":["useDeepAgentConnection","useAuiState","readRunLive"],"mappings":";;;;;;;;;;;;;;;;;;;AAYO,SAAS,mBAA4B;AAC1C,QAAM,aAAaA,aAAAA,uBAAA;AACnB,SAAOC,MAAAA,YAAY,CAAC,MAAMC,aAAAA,YAAY,EAAE,QAAQ,UAAU,CAAC;AAC7D;;"}
1
+ {"version":3,"file":"use-athena-run-live.cjs","sources":["../../src/chat/use-athena-run-live.ts"],"sourcesContent":["import { useAuiState } from '@assistant-ui/react';\nimport { readRunLive, useDeepAgentConnection } from '@athenaintel/agent-runtime';\n\n/**\n * Whether the thread has a live run — `thread.isRunning` folded through the\n * host's channel state, so a run behind a first dial (a new chat's first\n * send, switching back to a thread that was running) still reads as live\n * while a run behind a genuine reconnect or eviction does not. Surfaces\n * that gate on a live run (message edit, the voice card's live state, the\n * approval resume lock) read this instead of `thread.isRunning`, which\n * follows the presented status and blinks off for the attach round-trip.\n *\n * Scope: like every `useStatewire*` hook this reads the thread scope that\n * `AthenaChat` establishes, so call it from a component rendered inside\n * `AthenaChat` (a tool UI, a `components` override, a `composer` slot). A\n * sibling of `AthenaChat` under `AthenaProvider` has no thread scope and the\n * hook throws there; use `useAppendToComposer` / `useSendMessage` from such\n * placements instead.\n */\nexport function useAthenaRunLive(): boolean {\n const connection = useDeepAgentConnection();\n return useAuiState((s) => readRunLive(s.thread, connection));\n}\n"],"names":["useDeepAgentConnection","useAuiState","readRunLive"],"mappings":";;;;;;;;;;;;;;;;;;;AAmBO,SAAS,mBAA4B;AAC1C,QAAM,aAAaA,aAAAA,uBAAA;AACnB,SAAOC,MAAAA,YAAY,CAAC,MAAMC,aAAAA,YAAY,EAAE,QAAQ,UAAU,CAAC;AAC7D;;"}
@@ -6,5 +6,12 @@
6
6
  * that gate on a live run (message edit, the voice card's live state, the
7
7
  * approval resume lock) read this instead of `thread.isRunning`, which
8
8
  * follows the presented status and blinks off for the attach round-trip.
9
+ *
10
+ * Scope: like every `useStatewire*` hook this reads the thread scope that
11
+ * `AthenaChat` establishes, so call it from a component rendered inside
12
+ * `AthenaChat` (a tool UI, a `components` override, a `composer` slot). A
13
+ * sibling of `AthenaChat` under `AthenaProvider` has no thread scope and the
14
+ * hook throws there; use `useAppendToComposer` / `useSendMessage` from such
15
+ * placements instead.
9
16
  */
10
17
  export declare function useAthenaRunLive(): boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"use-athena-run-live.js","sources":["../../src/chat/use-athena-run-live.ts"],"sourcesContent":["import { useAuiState } from '@assistant-ui/react';\nimport { readRunLive, useDeepAgentConnection } from '@athenaintel/agent-runtime';\n\n/**\n * Whether the thread has a live run — `thread.isRunning` folded through the\n * host's channel state, so a run behind a first dial (a new chat's first\n * send, switching back to a thread that was running) still reads as live\n * while a run behind a genuine reconnect or eviction does not. Surfaces\n * that gate on a live run (message edit, the voice card's live state, the\n * approval resume lock) read this instead of `thread.isRunning`, which\n * follows the presented status and blinks off for the attach round-trip.\n */\nexport function useAthenaRunLive(): boolean {\n const connection = useDeepAgentConnection();\n return useAuiState((s) => readRunLive(s.thread, connection));\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAYO,SAAS,mBAA4B;AAC1C,QAAM,aAAa,uBAAA;AACnB,SAAO,YAAY,CAAC,MAAM,YAAY,EAAE,QAAQ,UAAU,CAAC;AAC7D;"}
1
+ {"version":3,"file":"use-athena-run-live.js","sources":["../../src/chat/use-athena-run-live.ts"],"sourcesContent":["import { useAuiState } from '@assistant-ui/react';\nimport { readRunLive, useDeepAgentConnection } from '@athenaintel/agent-runtime';\n\n/**\n * Whether the thread has a live run — `thread.isRunning` folded through the\n * host's channel state, so a run behind a first dial (a new chat's first\n * send, switching back to a thread that was running) still reads as live\n * while a run behind a genuine reconnect or eviction does not. Surfaces\n * that gate on a live run (message edit, the voice card's live state, the\n * approval resume lock) read this instead of `thread.isRunning`, which\n * follows the presented status and blinks off for the attach round-trip.\n *\n * Scope: like every `useStatewire*` hook this reads the thread scope that\n * `AthenaChat` establishes, so call it from a component rendered inside\n * `AthenaChat` (a tool UI, a `components` override, a `composer` slot). A\n * sibling of `AthenaChat` under `AthenaProvider` has no thread scope and the\n * hook throws there; use `useAppendToComposer` / `useSendMessage` from such\n * placements instead.\n */\nexport function useAthenaRunLive(): boolean {\n const connection = useDeepAgentConnection();\n return useAuiState((s) => readRunLive(s.thread, connection));\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAmBO,SAAS,mBAA4B;AAC1C,QAAM,aAAa,uBAAA;AACnB,SAAO,YAAY,CAAC,MAAM,YAAY,EAAE,QAAQ,UAAU,CAAC;AAC7D;"}
@@ -8,6 +8,7 @@ const react$2 = require("@tiptap/react");
8
8
  const Placeholder = require("@tiptap/extension-placeholder");
9
9
  const core = require("@tiptap/core");
10
10
  const react$1 = require("@assistant-ui/react");
11
+ const composerHandoff = require("../../composer/composer-handoff.cjs");
11
12
  const lucideReact = require("lucide-react");
12
13
  const chatTextSchema = require("../rich-text/chat-text-schema.cjs");
13
14
  const mentionSuggestions_extension = require("../mentions/suggestions/mention-suggestions.extension.cjs");
@@ -166,6 +167,7 @@ const TiptapComposer = react.forwardRef(
166
167
  }, outerRef) => {
167
168
  const aui = react$1.useAui();
168
169
  const composerInput = react$1.unstable_useComposerInput();
170
+ const handoff = composerHandoff.useComposerHandoffStore();
169
171
  const applyingRuntimeTextRef = react.useRef(false);
170
172
  const lastEmittedMarkdownRef = react.useRef(null);
171
173
  const listRef = react.useRef(null);
@@ -437,11 +439,24 @@ const TiptapComposer = react.forwardRef(
437
439
  applyingRuntimeTextRef.current = false;
438
440
  }
439
441
  onDraftStateChange == null ? void 0 : onDraftStateChange(value.trim().length > 0);
442
+ if (handoff == null ? void 0 : handoff.focusOnNextMirror) {
443
+ handoff.focusOnNextMirror = false;
444
+ editor.commands.focus("end");
445
+ }
440
446
  });
441
447
  return () => {
442
448
  cancelled = true;
443
449
  };
444
- }, [composerInput.value, editor, onDraftStateChange]);
450
+ }, [composerInput.value, editor, handoff, onDraftStateChange]);
451
+ react.useEffect(() => {
452
+ if (!editor || !handoff) return;
453
+ handoff.focusEnd = () => {
454
+ if (!editor.isDestroyed) editor.commands.focus("end");
455
+ };
456
+ return () => {
457
+ handoff.focusEnd = null;
458
+ };
459
+ }, [editor, handoff]);
445
460
  const insertAssetMention = react.useCallback(
446
461
  (asset, sourceFile) => {
447
462
  const currentEditor = editorRef.current;
@@ -1 +1 @@
1
- {"version":3,"file":"tiptap-composer.cjs","sources":["../../../src/chat-ui/composer/tiptap-composer.tsx"],"sourcesContent":["/**\n * Tiptap Composer — Full Olympus mention system integration\n *\n * Uses the ported Olympus infrastructure:\n * - MentionSuggestionList (with scope navigation, breadcrumbs, keyboard nav)\n * - MentionSuggestionPopup (Floating UI positioning)\n * - MentionSuggestionsExtension (Tiptap @ trigger)\n * - MentionExtension (inline mention chips)\n * - useMentionSuggestions (full Olympus categories + async data)\n *\n * Auth credentials for GraphQL are provided via MentionAuthCtx, and the host\n * app (Chrome side panel, Office task pane, the SDK's AthenaChat) supplies any\n * host-only mention items — browser tabs through `webpages`, its own Tools /\n * Toolkits list through `tools`.\n *\n * The draft serializes to Markdown through `tiptap-markdown` — the serializer\n * the Olympus chat composer sends with — so pasted hyperlinks keep their URL,\n * formatting marks survive, paragraphs stay separated, and a mention is the\n * `[@Name](url)` link the message renderer parses back into a chip.\n */\n\nimport { athenaDiagnostics } from \"../../diagnostics/athena-diagnostics\";\nimport {\n clipboardHasMeaningfulText,\n extractPastedImages,\n} from \"../../composer/clipboard\";\nimport {\n useRef,\n useState,\n useCallback,\n useEffect,\n forwardRef,\n useImperativeHandle,\n type DragEvent as ReactDragEvent,\n} from \"react\";\nimport { useEditor, EditorContent } from \"@tiptap/react\";\nimport Placeholder from \"@tiptap/extension-placeholder\";\nimport { Extension, type Editor } from \"@tiptap/core\";\nimport { unstable_useComposerInput, useAui } from \"@assistant-ui/react\";\nimport { LoaderCircleIcon, XIcon } from \"lucide-react\";\nimport { cn } from \"../../lib/utils\";\nimport { CHAT_TEXT_CLASS, createChatTextExtensions } from \"../rich-text/chat-text-schema\";\nimport {\n MentionSuggestionsExtension,\n type MentionSuggestionCallbacks,\n type MentionSuggestionRenderProps,\n} from \"../mentions/suggestions/mention-suggestions.extension\";\nimport { MentionSuggestionPopup } from \"../mentions/suggestions/components/mention-suggestion-popup\";\nimport {\n MentionSuggestionList,\n} from \"../mentions/suggestions/components/mention-suggestion-list\";\nimport {\n useMentionSuggestions,\n type MentionRootCategory,\n} from \"../mentions/suggestions/data/use-mention-suggestions\";\nimport type {\n MentionTool,\n MenuItem,\n WebpageMention,\n} from \"../mentions/suggestions/data/types\";\nimport type { KeyboardNavigationHandle } from \"../mentions/suggestions/components/use-keyboard-navigation\";\nimport {\n registerChatAssetMentionHandler,\n type UploadedChatAsset,\n} from \"@athenaintel/agent-runtime\";\nimport { PendingAttachmentUploads } from \"./pending-attachment-uploads\";\n\n// ── Submit keybind extension ──\n\nconst SubmitKeybind = Extension.create<{ onSubmit: () => void; isMentionOpen: () => boolean }>({\n name: \"submitKeybind\",\n priority: 1000,\n addOptions() {\n return { onSubmit: () => {}, isMentionOpen: () => false };\n },\n addKeyboardShortcuts() {\n return {\n Enter: () => {\n // Don't submit when the mention popup is open — let it handle Enter\n if (this.options.isMentionOpen()) return false;\n // Let the schema's keymaps continue lists, insert code newlines, and\n // split paragraphs inside structured blocks, as in the web composer.\n if ([\"listItem\", \"taskItem\", \"codeBlock\", \"table\", \"blockquote\"].some(\n (node) => this.editor.isActive(node),\n )) return false;\n this.options.onSubmit();\n return true;\n },\n \"Shift-Enter\": () => {\n return this.editor.commands.newlineInCode() || this.editor.commands.setHardBreak();\n },\n };\n },\n});\n\n// ── CSS for mention chips + editor ──\n\nconst MENTION_STYLES = `\n .image-preview-composer .mention-chip[data-display-as-preview=\"true\"] {\n display: none;\n }\n /* Scoped to this editor: hosts render other ProseMirror instances on the\n same page (AthenaChat's message renderer), which must keep their own\n height and placeholder rules. */\n .athena-chat-composer .ProseMirror {\n outline: none;\n min-height: 36px;\n max-height: 120px;\n overflow-y: auto;\n }\n .athena-chat-composer .ProseMirror p.is-editor-empty:first-child::before {\n content: attr(data-placeholder);\n float: left;\n color: #9ca3af;\n pointer-events: none;\n height: 0;\n }\n /* The paper appearance (the SDK thread): a taller resting height, room for\n about eight lines before the draft scrolls, and a placeholder at 35% of\n the text colour. Same specificity as the rules above, so order decides. */\n .athena-chat-composer--paper .ProseMirror {\n min-height: 44px;\n max-height: 12rem;\n }\n .athena-chat-composer--paper .ProseMirror p.is-editor-empty:first-child::before {\n color: color-mix(in srgb, currentColor 35%, transparent);\n }\n`;\n\n/**\n * The editor's look. `default` is what the Chrome extension and Office add-in\n * render; `paper` pairs with `ChatComposerDock appearance=\"paper\"`.\n */\nexport type TiptapComposerAppearance = \"default\" | \"paper\";\n\ninterface MarkdownStorage {\n getMarkdown(): string;\n}\n\n/** The draft as it is sent: Markdown, mentions as `[@Name](url)` links. */\nexport function serializeDraft(editor: Editor): string {\n return (editor.storage as { markdown: MarkdownStorage }).markdown.getMarkdown();\n}\n\n/**\n * A host's own intake for the files pasted into or dropped on the composer\n * (AthenaChat uploads them into its attachment pills). Without `onDrop` the\n * composer leaves drops untouched so an enclosing drop zone receives them.\n */\nexport interface ComposerFileIntake {\n onPaste: (files: readonly File[]) => void;\n onDrop?: (files: readonly File[]) => void;\n}\n\n// ── Main component ──\n\nexport interface TiptapComposerHandle {\n submit: () => Promise<void>;\n addAttachment: (file: File) => Promise<void>;\n insertAssetMention: (asset: UploadedChatAsset) => void;\n /**\n * Focuses the editor and types the `@` trigger at the caret (after a space\n * when the caret follows a word), opening the mention menu — the toolbar's\n * `@` button. A no-op while the editor is disabled or the menu is open.\n * Optional on the type so hosts' handle stand-ins written before it keep\n * type-checking; the composer itself always provides it.\n */\n openMentionMenu?: () => void;\n}\n\nexport interface TiptapComposerProps {\n placeholder?: string;\n threadId: string;\n disabled?: boolean;\n /** Editor look; defaults to the side-panel / task-pane editor. */\n appearance?: TiptapComposerAppearance;\n /** Host-provided browser-tab mentions; empty in hosts without tab access. */\n webpages?: readonly WebpageMention[];\n /**\n * Host-provided Tools / Toolkits mention candidates. When set they replace\n * the server tool catalog for those categories (see `useMentionSuggestions`).\n */\n tools?: readonly MentionTool[];\n rootCategories?: readonly MentionRootCategory[];\n onDraftStateChange?: (hasDraft: boolean) => void;\n /** Show local image thumbnails while mention-backed uploads are in the draft. */\n showImagePreviews?: boolean;\n /** Surface paste/drop attachment failures in host UI as well as diagnostics. */\n onAttachmentError?: (message: string) => void;\n /**\n * Host-owned intake for pasted and dropped files. Without it the composer\n * attaches them itself through `aui.composer.addAttachment`.\n */\n fileIntake?: ComposerFileIntake;\n /**\n * Awaited before every submit (Enter and the imperative handle), after the\n * composer's own pending uploads have settled: a host whose uploads live\n * outside the composer (AthenaChat's attachment pills) resolves once they\n * land, so a send never leaves without its attachment.\n */\n waitForHostUploads?: () => Promise<void> | void;\n /**\n * Optional submit interception: called with the serialized draft — possibly\n * empty, for a host composing its own extras into the send — before the\n * composer-runtime send. Return `true` when the host consumed the draft\n * through another lane (e.g. a live voice session) — the editor clears and\n * the normal send is skipped. Return `false` to fall through unchanged.\n */\n interceptSubmit?: (text: string) => boolean | Promise<boolean>;\n /**\n * Called after a draft has left the editor — through the composer-runtime\n * send or a host interception — from Enter and the imperative handle alike.\n * Hosts use it for per-send UI such as a first-use keyboard hint.\n */\n onSubmitted?: () => void;\n}\n\nconst NO_WEBPAGES: readonly WebpageMention[] = [];\n\ninterface ComposerImagePreview {\n key: string;\n file: File;\n name: string;\n src: string;\n assetId?: string;\n}\n\nfunction revokeImagePreview(src: string): void {\n if (typeof URL.revokeObjectURL === \"function\") URL.revokeObjectURL(src);\n}\n\nfunction ImagePreviewStrip({\n previews,\n onRemove,\n}: {\n previews: readonly ComposerImagePreview[];\n onRemove: (preview: ComposerImagePreview) => void;\n}) {\n if (previews.length === 0) return null;\n\n return (\n <div\n className=\"flex w-full gap-2 overflow-x-auto px-1.5 pb-1 pt-1.5\"\n role=\"list\"\n aria-label=\"Image attachments\"\n >\n {previews.map((preview) => {\n const uploading = preview.assetId === undefined;\n return (\n <div\n key={preview.key}\n className=\"group relative h-16 w-[clamp(76px,30vw,112px)] shrink-0 overflow-hidden rounded-lg border border-neutral-200 bg-neutral-100 shadow-sm\"\n role=\"listitem\"\n >\n <img\n src={preview.src}\n alt={preview.name}\n className=\"size-full object-cover\"\n />\n <div className=\"absolute inset-x-0 bottom-0 truncate bg-gradient-to-t from-black/75 to-transparent px-1.5 pb-1 pt-4 text-[10px] font-medium text-white\">\n {preview.name}\n </div>\n {uploading ? (\n <span\n className=\"absolute left-1 top-1 inline-flex size-5 items-center justify-center rounded-full bg-black/65 text-white\"\n title=\"Uploading image\"\n >\n <LoaderCircleIcon className=\"size-3 animate-spin\" />\n </span>\n ) : (\n <button\n type=\"button\"\n onClick={() => onRemove(preview)}\n className=\"absolute right-1 top-1 inline-flex size-5 items-center justify-center rounded-full bg-black/65 text-white opacity-80 transition-opacity hover:bg-black/80 hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white\"\n aria-label={`Remove ${preview.name}`}\n title=\"Remove image\"\n >\n <XIcon className=\"size-3\" />\n </button>\n )}\n </div>\n );\n })}\n </div>\n );\n}\n\nexport const TiptapComposer = forwardRef<\n TiptapComposerHandle,\n TiptapComposerProps\n>(\n (\n {\n placeholder = \"Message Athena... (type @ to mention)\",\n threadId,\n disabled = false,\n appearance = \"default\",\n webpages = NO_WEBPAGES,\n tools,\n rootCategories,\n onDraftStateChange,\n showImagePreviews = false,\n onAttachmentError,\n fileIntake,\n waitForHostUploads,\n interceptSubmit,\n onSubmitted,\n },\n outerRef,\n ) => {\n const aui = useAui();\n const composerInput = unstable_useComposerInput();\n const applyingRuntimeTextRef = useRef(false);\n // What this editor last mirrored into the composer runtime; a value equal to\n // it is our own echo and needs no re-serialization or restore.\n const lastEmittedMarkdownRef = useRef<string | null>(null);\n const listRef = useRef<KeyboardNavigationHandle>(null);\n const pendingUploadsRef = useRef(new PendingAttachmentUploads());\n const submitInFlightRef = useRef(false);\n const imagePreviewsRef = useRef<ComposerImagePreview[]>([]);\n const [imagePreviews, setImagePreviews] = useState<ComposerImagePreview[]>([]);\n\n const replaceImagePreviews = useCallback((next: ComposerImagePreview[]) => {\n imagePreviewsRef.current = next;\n setImagePreviews(next);\n }, []);\n\n const removeImagePreviews = useCallback(\n (shouldRemove: (preview: ComposerImagePreview) => boolean) => {\n const kept: ComposerImagePreview[] = [];\n let removed = false;\n for (const preview of imagePreviewsRef.current) {\n if (shouldRemove(preview)) {\n revokeImagePreview(preview.src);\n removed = true;\n } else kept.push(preview);\n }\n if (removed) replaceImagePreviews(kept);\n },\n [replaceImagePreviews],\n );\n\n const clearImagePreviews = useCallback(() => {\n removeImagePreviews(() => true);\n }, [removeImagePreviews]);\n\n useEffect(() => clearImagePreviews(), [clearImagePreviews, threadId]);\n useEffect(\n () => () => {\n for (const preview of imagePreviewsRef.current) {\n revokeImagePreview(preview.src);\n }\n imagePreviewsRef.current = [];\n },\n [],\n );\n\n const { store } = useMentionSuggestions({ webpages, tools, rootCategories });\n\n // Popup state\n const [popupOpen, setPopupOpen] = useState(false);\n const popupOpenRef = useRef(false);\n const [popupProps, setPopupProps] = useState<MentionSuggestionRenderProps | null>(null);\n\n // Suggestion callbacks — drive the popup lifecycle\n const callbacksRef = useRef<MentionSuggestionCallbacks>({\n onStart: (props) => {\n setPopupOpen(true);\n popupOpenRef.current = true;\n setPopupProps(props);\n },\n onUpdate: (props) => {\n setPopupProps(props);\n },\n onKeyDown: (event) => {\n // Route keyboard events to the MentionSuggestionList\n return listRef.current?.onKeyDown(event) ?? false;\n },\n onExit: () => {\n setPopupOpen(false);\n popupOpenRef.current = false;\n setPopupProps(null);\n },\n });\n\n // Editor ref — needed because handleSubmit is created before useEditor returns\n const editorRef = useRef<ReturnType<typeof useEditor>>(null);\n\n // Submit via composer runtime so attachments (screenshots, files) are included.\n // aui.composer.setText() + aui.composer.send() bundles text + pending attachments.\n //\n // Statewire turns a send during an active run into a queued command. Do not\n // cancel first: cancellation aborts the shared run for every connected\n // client and turns a normal queued follow-up into a destructive stop.\n const handleSubmit = useCallback(async () => {\n if (disabled) return;\n if (submitInFlightRef.current) return;\n submitInFlightRef.current = true;\n try {\n await pendingUploadsRef.current.waitForIdle();\n await waitForHostUploads?.();\n const ed = editorRef.current;\n if (!ed) return;\n const text = serializeDraft(ed).trim();\n\n // Interception carries TEXT only: a draft with pending attachments\n // (screenshots, files) must ride the composer-runtime send that\n // bundles them, or the attachments would silently vanish. Attachment\n // state is read only when a host actually intercepts. It runs before\n // the empty check because a host may compose extras of its own into\n // an otherwise empty draft (AthenaChat's quote and attachment pills).\n if (interceptSubmit) {\n const hasAttachments =\n (aui.composer.getState().attachments?.length ?? 0) > 0;\n if (!hasAttachments && (await interceptSubmit(text))) {\n ed.commands.clearContent();\n clearImagePreviews();\n onDraftStateChange?.(false);\n onSubmitted?.();\n return;\n }\n }\n\n if (!text) return;\n if (!composerInput.canSend) return;\n\n composerInput.setText(text);\n composerInput.send({ steer: false });\n\n ed.commands.clearContent();\n clearImagePreviews();\n onDraftStateChange?.(false);\n onSubmitted?.();\n } finally {\n submitInFlightRef.current = false;\n }\n }, [\n clearImagePreviews,\n composerInput,\n disabled,\n interceptSubmit,\n onDraftStateChange,\n onSubmitted,\n waitForHostUploads,\n ]);\n\n const handleSubmitRef = useRef(handleSubmit);\n handleSubmitRef.current = handleSubmit;\n\n const addAttachment = useCallback(\n (file: File) => {\n let previewKey: string | undefined;\n if (\n showImagePreviews &&\n file.type.startsWith(\"image/\") &&\n typeof URL.createObjectURL === \"function\"\n ) {\n previewKey = crypto.randomUUID();\n replaceImagePreviews([\n ...imagePreviewsRef.current,\n {\n key: previewKey,\n file,\n name: file.name || \"Pasted image\",\n src: URL.createObjectURL(file),\n },\n ]);\n }\n\n return pendingUploadsRef.current\n .track(aui.composer.addAttachment(file))\n .catch((error: unknown) => {\n if (previewKey) {\n removeImagePreviews((preview) => preview.key === previewKey);\n }\n throw error;\n });\n },\n [aui, removeImagePreviews, replaceImagePreviews, showImagePreviews],\n );\n\n // Handle paste events — intercept images from clipboard and add as attachments\n const handlePaste = useCallback(\n (_view: unknown, event: ClipboardEvent) => {\n const clipboardData = event.clipboardData;\n if (!clipboardData) return false;\n\n const images = extractPastedImages({ clipboardData });\n if (images.length === 0) return false;\n // Office and spreadsheet copies put the cells' text beside a bitmap of\n // them: the text is what the user copied, so the editor's native paste\n // keeps it and the incidental screenshot is left alone. A copied image\n // file's own name (Finder, Explorer) is not such text.\n if (\n clipboardHasMeaningfulText({\n clipboardData,\n pastedFileNames: images.map((image) => image.originalName),\n })\n ) {\n return false;\n }\n event.preventDefault();\n\n const files = images.map((image) => image.file);\n if (fileIntake) {\n fileIntake.onPaste(files);\n return true;\n }\n for (const file of files) {\n void addAttachment(file).catch((error: unknown) => {\n onAttachmentError?.(\"Couldn’t paste that image.\");\n athenaDiagnostics.error(\n \"composer.attachment.error\",\n error,\n { code: \"attachment_failed\", message: \"Pasted image upload failed\" },\n { source: \"paste\", type: file.type },\n );\n });\n }\n return true;\n },\n [addAttachment, fileIntake, onAttachmentError],\n );\n\n // Without a host intake the composer attaches dropped images itself; a host\n // intake without `onDrop` leaves drops to the enclosing drop zone.\n const addDroppedImages = useCallback(\n (files: FileList | readonly File[]) => {\n const images = Array.from(files).filter((file) =>\n file.type.startsWith(\"image/\"),\n );\n if (images.length === 0) return false;\n if (fileIntake) {\n if (!fileIntake.onDrop) return false;\n fileIntake.onDrop(images);\n return true;\n }\n for (const file of images) {\n void addAttachment(file).catch((error: unknown) => {\n onAttachmentError?.(\"Couldn’t attach that dropped image.\");\n athenaDiagnostics.error(\n \"composer.attachment.error\",\n error,\n { code: \"attachment_failed\", message: \"Dropped image upload failed\" },\n { source: \"drop\", type: file.type },\n );\n });\n }\n return true;\n },\n [addAttachment, fileIntake, onAttachmentError],\n );\n\n // ProseMirror owns drops that land in the editable surface. The React\n // wrapper below catches the rest of the composer area; defaultPrevented\n // keeps one gesture from being attached twice.\n const handleDrop = useCallback(\n (_view: unknown, event: DragEvent) => {\n const files = event.dataTransfer?.files;\n if (!files?.length) return false;\n const handled = addDroppedImages(files);\n if (handled) event.preventDefault();\n return handled;\n },\n [addDroppedImages],\n );\n\n const handleComposerDrop = useCallback(\n (event: ReactDragEvent<HTMLDivElement>) => {\n if (event.defaultPrevented) return;\n if (addDroppedImages(event.dataTransfer.files)) event.preventDefault();\n },\n [addDroppedImages],\n );\n\n const editor = useEditor({\n editable: !disabled,\n extensions: [\n ...createChatTextExtensions(),\n Placeholder.configure({ placeholder }),\n MentionSuggestionsExtension.configure({\n store,\n callbacks: callbacksRef.current,\n }),\n SubmitKeybind.configure({\n onSubmit: () => void handleSubmitRef.current(),\n isMentionOpen: () => popupOpenRef.current,\n }),\n ],\n editorProps: {\n attributes: {\n class: cn(CHAT_TEXT_CLASS, \"w-full resize-none bg-transparent px-1.5 py-2 outline-none\"),\n \"data-testid\": \"athena-chat-composer\",\n },\n handlePaste,\n handleDrop,\n },\n onUpdate: ({ editor: updatedEditor }) => {\n const nextText = serializeDraft(updatedEditor);\n lastEmittedMarkdownRef.current = nextText;\n if (!applyingRuntimeTextRef.current) {\n composerInput.setText(nextText);\n }\n onDraftStateChange?.(nextText.trim().length > 0);\n\n if (showImagePreviews) {\n const mentionedAssetIds = new Set<string>();\n updatedEditor.state.doc.descendants((node) => {\n if (\n node.type.name === \"mention\" &&\n node.attrs.type === \"asset\" &&\n typeof node.attrs.id === \"string\"\n ) {\n mentionedAssetIds.add(node.attrs.id);\n }\n });\n removeImagePreviews(\n (preview) =>\n preview.assetId !== undefined &&\n !mentionedAssetIds.has(preview.assetId),\n );\n }\n },\n immediatelyRender: false,\n });\n\n // Keep ref in sync so handleSubmit always has the editor\n editorRef.current = editor;\n\n useEffect(() => {\n editor?.setEditable(!disabled);\n }, [disabled, editor]);\n\n // This rich editor replaces ComposerPrimitive.Input, so it must mirror its\n // draft into assistant-ui's composer state. Statewire restores that state\n // when a send is rejected during reconnect, and a host may write to it\n // (`useAppendToComposer`); mirroring it back into Tiptap — parsed as the\n // Markdown it was serialized to, mention links included — preserves the\n // user's message instead of leaving an empty composer. Our own echo is\n // skipped on the ref; a restore's re-serialization may differ from the\n // value byte-for-byte, so the ref takes that form to stop a second pass.\n useEffect(() => {\n if (!editor) return;\n const value = composerInput.value;\n if (value === lastEmittedMarkdownRef.current) return;\n const currentMarkdown = serializeDraft(editor);\n lastEmittedMarkdownRef.current = currentMarkdown;\n if (value === currentMarkdown) return;\n // React node views flush their mounts synchronously. Restore outside\n // React's effect phase, and cancel a restore superseded by a newer draft.\n let cancelled = false;\n queueMicrotask(() => {\n if (cancelled || editor.isDestroyed) return;\n applyingRuntimeTextRef.current = true;\n try {\n editor.commands.setContent(value);\n lastEmittedMarkdownRef.current = serializeDraft(editor);\n } finally {\n applyingRuntimeTextRef.current = false;\n }\n onDraftStateChange?.(value.trim().length > 0);\n });\n return () => { cancelled = true; };\n }, [composerInput.value, editor, onDraftStateChange]);\n\n const insertAssetMention = useCallback(\n (asset: UploadedChatAsset, sourceFile?: File) => {\n const currentEditor = editorRef.current;\n if (!currentEditor) throw new Error(\"The chat composer is not ready.\");\n currentEditor\n .chain()\n .focus()\n .insertContent({\n type: \"mention\",\n attrs: {\n type: \"asset\",\n name: asset.title,\n id: asset.id,\n assetType: asset.assetType,\n displayAsPreview:\n showImagePreviews && sourceFile?.type.startsWith(\"image/\"),\n },\n })\n // A text node, not a string: the Markdown extension parses string\n // content as Markdown, which would drop a lone space.\n .insertContent({ type: \"text\", text: \" \" })\n .run();\n\n if (\n showImagePreviews &&\n sourceFile?.type.startsWith(\"image/\") &&\n typeof URL.createObjectURL === \"function\"\n ) {\n const existing = imagePreviewsRef.current.find(\n (preview) => preview.file === sourceFile,\n );\n if (existing) {\n replaceImagePreviews(\n imagePreviewsRef.current.map((preview) =>\n preview.key === existing.key\n ? { ...preview, assetId: asset.id, name: asset.title }\n : preview,\n ),\n );\n } else {\n replaceImagePreviews([\n ...imagePreviewsRef.current,\n {\n key: crypto.randomUUID(),\n file: sourceFile,\n name: asset.title,\n src: URL.createObjectURL(sourceFile),\n assetId: asset.id,\n },\n ]);\n }\n }\n },\n [replaceImagePreviews, showImagePreviews],\n );\n\n const removeImagePreview = useCallback(\n (preview: ComposerImagePreview) => {\n if (!preview.assetId) return;\n const currentEditor = editorRef.current;\n if (!currentEditor) return;\n const positions: Array<{ from: number; to: number }> = [];\n currentEditor.state.doc.descendants((node, position) => {\n if (\n node.type.name === \"mention\" &&\n node.attrs.type === \"asset\" &&\n node.attrs.id === preview.assetId\n ) {\n positions.push({ from: position, to: position + node.nodeSize });\n }\n });\n let transaction = currentEditor.state.tr;\n for (const position of positions.reverse()) {\n transaction = transaction.delete(position.from, position.to);\n }\n if (transaction.docChanged) currentEditor.view.dispatch(transaction);\n removeImagePreviews((candidate) => candidate.key === preview.key);\n currentEditor.commands.focus();\n },\n [removeImagePreviews],\n );\n\n useEffect(\n () => registerChatAssetMentionHandler(threadId, insertAssetMention),\n [insertAssetMention, threadId],\n );\n\n const openMentionMenu = useCallback(() => {\n if (disabled) return;\n const currentEditor = editorRef.current;\n if (!currentEditor || currentEditor.isDestroyed) return;\n if (popupOpenRef.current) {\n currentEditor.commands.focus();\n return;\n }\n // The suggestion plugin only opens on an `@` that starts a word\n // (`allowedPrefixes`), so a trigger typed right after text — or after a\n // mention chip, which `textBetween` reports as its leaf text — gets the\n // separating space a user would type.\n const { from } = currentEditor.state.selection;\n const before =\n from > 0\n ? currentEditor.state.doc.textBetween(from - 1, from, \"\\0\", \"\\0\")\n : \"\";\n const needsSpace = before.length > 0 && !/[\\s([{\"']/.test(before);\n currentEditor\n .chain()\n .focus()\n .insertContent({ type: \"text\", text: needsSpace ? \" @\" : \"@\" })\n .run();\n }, [disabled]);\n\n useImperativeHandle(\n outerRef,\n () => ({\n submit: handleSubmit,\n addAttachment,\n insertAssetMention,\n openMentionMenu,\n }),\n [addAttachment, handleSubmit, insertAssetMention, openMentionMenu],\n );\n\n return (\n <>\n <style>{MENTION_STYLES}</style>\n <div\n className={cn(\n \"athena-chat-composer flex-1 min-w-0\",\n appearance === \"paper\" && \"athena-chat-composer--paper\",\n showImagePreviews && \"image-preview-composer\",\n )}\n aria-label=\"Message composer\"\n aria-disabled={disabled}\n onDragOver={(event) => {\n if (Array.from(event.dataTransfer.types).includes(\"Files\")) {\n event.preventDefault();\n event.dataTransfer.dropEffect = \"copy\";\n }\n }}\n onDrop={handleComposerDrop}\n >\n <ImagePreviewStrip\n previews={imagePreviews}\n onRemove={removeImagePreview}\n />\n <EditorContent editor={editor} />\n </div>\n\n {/* Olympus-style mention popup */}\n {popupOpen && popupProps && editor && (\n <MentionSuggestionPopup clientRect={popupProps.clientRect}>\n <MentionSuggestionList\n ref={listRef}\n store={store}\n editor={editor}\n query={popupProps.query}\n command={(item: MenuItem) => popupProps.command(item)}\n closeMenu={() => {\n setPopupOpen(false);\n editor.commands.focus();\n }}\n />\n </MentionSuggestionPopup>\n )}\n </>\n );\n },\n);\n\nTiptapComposer.displayName = \"TiptapComposer\";\n"],"names":["Extension","jsx","jsxs","LoaderCircleIcon","XIcon","forwardRef","useAui","unstable_useComposerInput","useRef","PendingAttachmentUploads","useState","useCallback","useEffect","useMentionSuggestions","extractPastedImages","clipboardHasMeaningfulText","athenaDiagnostics","useEditor","createChatTextExtensions","MentionSuggestionsExtension","cn","CHAT_TEXT_CLASS","registerChatAssetMentionHandler","useImperativeHandle","Fragment","EditorContent","MentionSuggestionPopup","MentionSuggestionList"],"mappings":"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"}
1
+ {"version":3,"file":"tiptap-composer.cjs","sources":["../../../src/chat-ui/composer/tiptap-composer.tsx"],"sourcesContent":["/**\n * Tiptap Composer — Full Olympus mention system integration\n *\n * Uses the ported Olympus infrastructure:\n * - MentionSuggestionList (with scope navigation, breadcrumbs, keyboard nav)\n * - MentionSuggestionPopup (Floating UI positioning)\n * - MentionSuggestionsExtension (Tiptap @ trigger)\n * - MentionExtension (inline mention chips)\n * - useMentionSuggestions (full Olympus categories + async data)\n *\n * Auth credentials for GraphQL are provided via MentionAuthCtx, and the host\n * app (Chrome side panel, Office task pane, the SDK's AthenaChat) supplies any\n * host-only mention items — browser tabs through `webpages`, its own Tools /\n * Toolkits list through `tools`.\n *\n * The draft serializes to Markdown through `tiptap-markdown` — the serializer\n * the Olympus chat composer sends with — so pasted hyperlinks keep their URL,\n * formatting marks survive, paragraphs stay separated, and a mention is the\n * `[@Name](url)` link the message renderer parses back into a chip.\n */\n\nimport { athenaDiagnostics } from \"../../diagnostics/athena-diagnostics\";\nimport {\n clipboardHasMeaningfulText,\n extractPastedImages,\n} from \"../../composer/clipboard\";\nimport {\n useRef,\n useState,\n useCallback,\n useEffect,\n forwardRef,\n useImperativeHandle,\n type DragEvent as ReactDragEvent,\n} from \"react\";\nimport { useEditor, EditorContent } from \"@tiptap/react\";\nimport Placeholder from \"@tiptap/extension-placeholder\";\nimport { Extension, type Editor } from \"@tiptap/core\";\nimport { unstable_useComposerInput, useAui } from \"@assistant-ui/react\";\nimport { useComposerHandoffStore } from \"../../composer/composer-handoff\";\nimport { LoaderCircleIcon, XIcon } from \"lucide-react\";\nimport { cn } from \"../../lib/utils\";\nimport { CHAT_TEXT_CLASS, createChatTextExtensions } from \"../rich-text/chat-text-schema\";\nimport {\n MentionSuggestionsExtension,\n type MentionSuggestionCallbacks,\n type MentionSuggestionRenderProps,\n} from \"../mentions/suggestions/mention-suggestions.extension\";\nimport { MentionSuggestionPopup } from \"../mentions/suggestions/components/mention-suggestion-popup\";\nimport {\n MentionSuggestionList,\n} from \"../mentions/suggestions/components/mention-suggestion-list\";\nimport {\n useMentionSuggestions,\n type MentionRootCategory,\n} from \"../mentions/suggestions/data/use-mention-suggestions\";\nimport type {\n MentionTool,\n MenuItem,\n WebpageMention,\n} from \"../mentions/suggestions/data/types\";\nimport type { KeyboardNavigationHandle } from \"../mentions/suggestions/components/use-keyboard-navigation\";\nimport {\n registerChatAssetMentionHandler,\n type UploadedChatAsset,\n} from \"@athenaintel/agent-runtime\";\nimport { PendingAttachmentUploads } from \"./pending-attachment-uploads\";\n\n// ── Submit keybind extension ──\n\nconst SubmitKeybind = Extension.create<{ onSubmit: () => void; isMentionOpen: () => boolean }>({\n name: \"submitKeybind\",\n priority: 1000,\n addOptions() {\n return { onSubmit: () => {}, isMentionOpen: () => false };\n },\n addKeyboardShortcuts() {\n return {\n Enter: () => {\n // Don't submit when the mention popup is open — let it handle Enter\n if (this.options.isMentionOpen()) return false;\n // Let the schema's keymaps continue lists, insert code newlines, and\n // split paragraphs inside structured blocks, as in the web composer.\n if ([\"listItem\", \"taskItem\", \"codeBlock\", \"table\", \"blockquote\"].some(\n (node) => this.editor.isActive(node),\n )) return false;\n this.options.onSubmit();\n return true;\n },\n \"Shift-Enter\": () => {\n return this.editor.commands.newlineInCode() || this.editor.commands.setHardBreak();\n },\n };\n },\n});\n\n// ── CSS for mention chips + editor ──\n\nconst MENTION_STYLES = `\n .image-preview-composer .mention-chip[data-display-as-preview=\"true\"] {\n display: none;\n }\n /* Scoped to this editor: hosts render other ProseMirror instances on the\n same page (AthenaChat's message renderer), which must keep their own\n height and placeholder rules. */\n .athena-chat-composer .ProseMirror {\n outline: none;\n min-height: 36px;\n max-height: 120px;\n overflow-y: auto;\n }\n .athena-chat-composer .ProseMirror p.is-editor-empty:first-child::before {\n content: attr(data-placeholder);\n float: left;\n color: #9ca3af;\n pointer-events: none;\n height: 0;\n }\n /* The paper appearance (the SDK thread): a taller resting height, room for\n about eight lines before the draft scrolls, and a placeholder at 35% of\n the text colour. Same specificity as the rules above, so order decides. */\n .athena-chat-composer--paper .ProseMirror {\n min-height: 44px;\n max-height: 12rem;\n }\n .athena-chat-composer--paper .ProseMirror p.is-editor-empty:first-child::before {\n color: color-mix(in srgb, currentColor 35%, transparent);\n }\n`;\n\n/**\n * The editor's look. `default` is what the Chrome extension and Office add-in\n * render; `paper` pairs with `ChatComposerDock appearance=\"paper\"`.\n */\nexport type TiptapComposerAppearance = \"default\" | \"paper\";\n\ninterface MarkdownStorage {\n getMarkdown(): string;\n}\n\n/** The draft as it is sent: Markdown, mentions as `[@Name](url)` links. */\nexport function serializeDraft(editor: Editor): string {\n return (editor.storage as { markdown: MarkdownStorage }).markdown.getMarkdown();\n}\n\n/**\n * A host's own intake for the files pasted into or dropped on the composer\n * (AthenaChat uploads them into its attachment pills). Without `onDrop` the\n * composer leaves drops untouched so an enclosing drop zone receives them.\n */\nexport interface ComposerFileIntake {\n onPaste: (files: readonly File[]) => void;\n onDrop?: (files: readonly File[]) => void;\n}\n\n// ── Main component ──\n\nexport interface TiptapComposerHandle {\n submit: () => Promise<void>;\n addAttachment: (file: File) => Promise<void>;\n insertAssetMention: (asset: UploadedChatAsset) => void;\n /**\n * Focuses the editor and types the `@` trigger at the caret (after a space\n * when the caret follows a word), opening the mention menu — the toolbar's\n * `@` button. A no-op while the editor is disabled or the menu is open.\n * Optional on the type so hosts' handle stand-ins written before it keep\n * type-checking; the composer itself always provides it.\n */\n openMentionMenu?: () => void;\n}\n\nexport interface TiptapComposerProps {\n placeholder?: string;\n threadId: string;\n disabled?: boolean;\n /** Editor look; defaults to the side-panel / task-pane editor. */\n appearance?: TiptapComposerAppearance;\n /** Host-provided browser-tab mentions; empty in hosts without tab access. */\n webpages?: readonly WebpageMention[];\n /**\n * Host-provided Tools / Toolkits mention candidates. When set they replace\n * the server tool catalog for those categories (see `useMentionSuggestions`).\n */\n tools?: readonly MentionTool[];\n rootCategories?: readonly MentionRootCategory[];\n onDraftStateChange?: (hasDraft: boolean) => void;\n /** Show local image thumbnails while mention-backed uploads are in the draft. */\n showImagePreviews?: boolean;\n /** Surface paste/drop attachment failures in host UI as well as diagnostics. */\n onAttachmentError?: (message: string) => void;\n /**\n * Host-owned intake for pasted and dropped files. Without it the composer\n * attaches them itself through `aui.composer.addAttachment`.\n */\n fileIntake?: ComposerFileIntake;\n /**\n * Awaited before every submit (Enter and the imperative handle), after the\n * composer's own pending uploads have settled: a host whose uploads live\n * outside the composer (AthenaChat's attachment pills) resolves once they\n * land, so a send never leaves without its attachment.\n */\n waitForHostUploads?: () => Promise<void> | void;\n /**\n * Optional submit interception: called with the serialized draft — possibly\n * empty, for a host composing its own extras into the send — before the\n * composer-runtime send. Return `true` when the host consumed the draft\n * through another lane (e.g. a live voice session) — the editor clears and\n * the normal send is skipped. Return `false` to fall through unchanged.\n */\n interceptSubmit?: (text: string) => boolean | Promise<boolean>;\n /**\n * Called after a draft has left the editor — through the composer-runtime\n * send or a host interception — from Enter and the imperative handle alike.\n * Hosts use it for per-send UI such as a first-use keyboard hint.\n */\n onSubmitted?: () => void;\n}\n\nconst NO_WEBPAGES: readonly WebpageMention[] = [];\n\ninterface ComposerImagePreview {\n key: string;\n file: File;\n name: string;\n src: string;\n assetId?: string;\n}\n\nfunction revokeImagePreview(src: string): void {\n if (typeof URL.revokeObjectURL === \"function\") URL.revokeObjectURL(src);\n}\n\nfunction ImagePreviewStrip({\n previews,\n onRemove,\n}: {\n previews: readonly ComposerImagePreview[];\n onRemove: (preview: ComposerImagePreview) => void;\n}) {\n if (previews.length === 0) return null;\n\n return (\n <div\n className=\"flex w-full gap-2 overflow-x-auto px-1.5 pb-1 pt-1.5\"\n role=\"list\"\n aria-label=\"Image attachments\"\n >\n {previews.map((preview) => {\n const uploading = preview.assetId === undefined;\n return (\n <div\n key={preview.key}\n className=\"group relative h-16 w-[clamp(76px,30vw,112px)] shrink-0 overflow-hidden rounded-lg border border-neutral-200 bg-neutral-100 shadow-sm\"\n role=\"listitem\"\n >\n <img\n src={preview.src}\n alt={preview.name}\n className=\"size-full object-cover\"\n />\n <div className=\"absolute inset-x-0 bottom-0 truncate bg-gradient-to-t from-black/75 to-transparent px-1.5 pb-1 pt-4 text-[10px] font-medium text-white\">\n {preview.name}\n </div>\n {uploading ? (\n <span\n className=\"absolute left-1 top-1 inline-flex size-5 items-center justify-center rounded-full bg-black/65 text-white\"\n title=\"Uploading image\"\n >\n <LoaderCircleIcon className=\"size-3 animate-spin\" />\n </span>\n ) : (\n <button\n type=\"button\"\n onClick={() => onRemove(preview)}\n className=\"absolute right-1 top-1 inline-flex size-5 items-center justify-center rounded-full bg-black/65 text-white opacity-80 transition-opacity hover:bg-black/80 hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white\"\n aria-label={`Remove ${preview.name}`}\n title=\"Remove image\"\n >\n <XIcon className=\"size-3\" />\n </button>\n )}\n </div>\n );\n })}\n </div>\n );\n}\n\nexport const TiptapComposer = forwardRef<\n TiptapComposerHandle,\n TiptapComposerProps\n>(\n (\n {\n placeholder = \"Message Athena... (type @ to mention)\",\n threadId,\n disabled = false,\n appearance = \"default\",\n webpages = NO_WEBPAGES,\n tools,\n rootCategories,\n onDraftStateChange,\n showImagePreviews = false,\n onAttachmentError,\n fileIntake,\n waitForHostUploads,\n interceptSubmit,\n onSubmitted,\n },\n outerRef,\n ) => {\n const aui = useAui();\n const composerInput = unstable_useComposerInput();\n const handoff = useComposerHandoffStore();\n const applyingRuntimeTextRef = useRef(false);\n // What this editor last mirrored into the composer runtime; a value equal to\n // it is our own echo and needs no re-serialization or restore.\n const lastEmittedMarkdownRef = useRef<string | null>(null);\n const listRef = useRef<KeyboardNavigationHandle>(null);\n const pendingUploadsRef = useRef(new PendingAttachmentUploads());\n const submitInFlightRef = useRef(false);\n const imagePreviewsRef = useRef<ComposerImagePreview[]>([]);\n const [imagePreviews, setImagePreviews] = useState<ComposerImagePreview[]>([]);\n\n const replaceImagePreviews = useCallback((next: ComposerImagePreview[]) => {\n imagePreviewsRef.current = next;\n setImagePreviews(next);\n }, []);\n\n const removeImagePreviews = useCallback(\n (shouldRemove: (preview: ComposerImagePreview) => boolean) => {\n const kept: ComposerImagePreview[] = [];\n let removed = false;\n for (const preview of imagePreviewsRef.current) {\n if (shouldRemove(preview)) {\n revokeImagePreview(preview.src);\n removed = true;\n } else kept.push(preview);\n }\n if (removed) replaceImagePreviews(kept);\n },\n [replaceImagePreviews],\n );\n\n const clearImagePreviews = useCallback(() => {\n removeImagePreviews(() => true);\n }, [removeImagePreviews]);\n\n useEffect(() => clearImagePreviews(), [clearImagePreviews, threadId]);\n useEffect(\n () => () => {\n for (const preview of imagePreviewsRef.current) {\n revokeImagePreview(preview.src);\n }\n imagePreviewsRef.current = [];\n },\n [],\n );\n\n const { store } = useMentionSuggestions({ webpages, tools, rootCategories });\n\n // Popup state\n const [popupOpen, setPopupOpen] = useState(false);\n const popupOpenRef = useRef(false);\n const [popupProps, setPopupProps] = useState<MentionSuggestionRenderProps | null>(null);\n\n // Suggestion callbacks — drive the popup lifecycle\n const callbacksRef = useRef<MentionSuggestionCallbacks>({\n onStart: (props) => {\n setPopupOpen(true);\n popupOpenRef.current = true;\n setPopupProps(props);\n },\n onUpdate: (props) => {\n setPopupProps(props);\n },\n onKeyDown: (event) => {\n // Route keyboard events to the MentionSuggestionList\n return listRef.current?.onKeyDown(event) ?? false;\n },\n onExit: () => {\n setPopupOpen(false);\n popupOpenRef.current = false;\n setPopupProps(null);\n },\n });\n\n // Editor ref — needed because handleSubmit is created before useEditor returns\n const editorRef = useRef<ReturnType<typeof useEditor>>(null);\n\n // Submit via composer runtime so attachments (screenshots, files) are included.\n // aui.composer.setText() + aui.composer.send() bundles text + pending attachments.\n //\n // Statewire turns a send during an active run into a queued command. Do not\n // cancel first: cancellation aborts the shared run for every connected\n // client and turns a normal queued follow-up into a destructive stop.\n const handleSubmit = useCallback(async () => {\n if (disabled) return;\n if (submitInFlightRef.current) return;\n submitInFlightRef.current = true;\n try {\n await pendingUploadsRef.current.waitForIdle();\n await waitForHostUploads?.();\n const ed = editorRef.current;\n if (!ed) return;\n const text = serializeDraft(ed).trim();\n\n // Interception carries TEXT only: a draft with pending attachments\n // (screenshots, files) must ride the composer-runtime send that\n // bundles them, or the attachments would silently vanish. Attachment\n // state is read only when a host actually intercepts. It runs before\n // the empty check because a host may compose extras of its own into\n // an otherwise empty draft (AthenaChat's quote and attachment pills).\n if (interceptSubmit) {\n const hasAttachments =\n (aui.composer.getState().attachments?.length ?? 0) > 0;\n if (!hasAttachments && (await interceptSubmit(text))) {\n ed.commands.clearContent();\n clearImagePreviews();\n onDraftStateChange?.(false);\n onSubmitted?.();\n return;\n }\n }\n\n if (!text) return;\n if (!composerInput.canSend) return;\n\n composerInput.setText(text);\n composerInput.send({ steer: false });\n\n ed.commands.clearContent();\n clearImagePreviews();\n onDraftStateChange?.(false);\n onSubmitted?.();\n } finally {\n submitInFlightRef.current = false;\n }\n }, [\n clearImagePreviews,\n composerInput,\n disabled,\n interceptSubmit,\n onDraftStateChange,\n onSubmitted,\n waitForHostUploads,\n ]);\n\n const handleSubmitRef = useRef(handleSubmit);\n handleSubmitRef.current = handleSubmit;\n\n const addAttachment = useCallback(\n (file: File) => {\n let previewKey: string | undefined;\n if (\n showImagePreviews &&\n file.type.startsWith(\"image/\") &&\n typeof URL.createObjectURL === \"function\"\n ) {\n previewKey = crypto.randomUUID();\n replaceImagePreviews([\n ...imagePreviewsRef.current,\n {\n key: previewKey,\n file,\n name: file.name || \"Pasted image\",\n src: URL.createObjectURL(file),\n },\n ]);\n }\n\n return pendingUploadsRef.current\n .track(aui.composer.addAttachment(file))\n .catch((error: unknown) => {\n if (previewKey) {\n removeImagePreviews((preview) => preview.key === previewKey);\n }\n throw error;\n });\n },\n [aui, removeImagePreviews, replaceImagePreviews, showImagePreviews],\n );\n\n // Handle paste events — intercept images from clipboard and add as attachments\n const handlePaste = useCallback(\n (_view: unknown, event: ClipboardEvent) => {\n const clipboardData = event.clipboardData;\n if (!clipboardData) return false;\n\n const images = extractPastedImages({ clipboardData });\n if (images.length === 0) return false;\n // Office and spreadsheet copies put the cells' text beside a bitmap of\n // them: the text is what the user copied, so the editor's native paste\n // keeps it and the incidental screenshot is left alone. A copied image\n // file's own name (Finder, Explorer) is not such text.\n if (\n clipboardHasMeaningfulText({\n clipboardData,\n pastedFileNames: images.map((image) => image.originalName),\n })\n ) {\n return false;\n }\n event.preventDefault();\n\n const files = images.map((image) => image.file);\n if (fileIntake) {\n fileIntake.onPaste(files);\n return true;\n }\n for (const file of files) {\n void addAttachment(file).catch((error: unknown) => {\n onAttachmentError?.(\"Couldn’t paste that image.\");\n athenaDiagnostics.error(\n \"composer.attachment.error\",\n error,\n { code: \"attachment_failed\", message: \"Pasted image upload failed\" },\n { source: \"paste\", type: file.type },\n );\n });\n }\n return true;\n },\n [addAttachment, fileIntake, onAttachmentError],\n );\n\n // Without a host intake the composer attaches dropped images itself; a host\n // intake without `onDrop` leaves drops to the enclosing drop zone.\n const addDroppedImages = useCallback(\n (files: FileList | readonly File[]) => {\n const images = Array.from(files).filter((file) =>\n file.type.startsWith(\"image/\"),\n );\n if (images.length === 0) return false;\n if (fileIntake) {\n if (!fileIntake.onDrop) return false;\n fileIntake.onDrop(images);\n return true;\n }\n for (const file of images) {\n void addAttachment(file).catch((error: unknown) => {\n onAttachmentError?.(\"Couldn’t attach that dropped image.\");\n athenaDiagnostics.error(\n \"composer.attachment.error\",\n error,\n { code: \"attachment_failed\", message: \"Dropped image upload failed\" },\n { source: \"drop\", type: file.type },\n );\n });\n }\n return true;\n },\n [addAttachment, fileIntake, onAttachmentError],\n );\n\n // ProseMirror owns drops that land in the editable surface. The React\n // wrapper below catches the rest of the composer area; defaultPrevented\n // keeps one gesture from being attached twice.\n const handleDrop = useCallback(\n (_view: unknown, event: DragEvent) => {\n const files = event.dataTransfer?.files;\n if (!files?.length) return false;\n const handled = addDroppedImages(files);\n if (handled) event.preventDefault();\n return handled;\n },\n [addDroppedImages],\n );\n\n const handleComposerDrop = useCallback(\n (event: ReactDragEvent<HTMLDivElement>) => {\n if (event.defaultPrevented) return;\n if (addDroppedImages(event.dataTransfer.files)) event.preventDefault();\n },\n [addDroppedImages],\n );\n\n const editor = useEditor({\n editable: !disabled,\n extensions: [\n ...createChatTextExtensions(),\n Placeholder.configure({ placeholder }),\n MentionSuggestionsExtension.configure({\n store,\n callbacks: callbacksRef.current,\n }),\n SubmitKeybind.configure({\n onSubmit: () => void handleSubmitRef.current(),\n isMentionOpen: () => popupOpenRef.current,\n }),\n ],\n editorProps: {\n attributes: {\n class: cn(CHAT_TEXT_CLASS, \"w-full resize-none bg-transparent px-1.5 py-2 outline-none\"),\n \"data-testid\": \"athena-chat-composer\",\n },\n handlePaste,\n handleDrop,\n },\n onUpdate: ({ editor: updatedEditor }) => {\n const nextText = serializeDraft(updatedEditor);\n lastEmittedMarkdownRef.current = nextText;\n if (!applyingRuntimeTextRef.current) {\n composerInput.setText(nextText);\n }\n onDraftStateChange?.(nextText.trim().length > 0);\n\n if (showImagePreviews) {\n const mentionedAssetIds = new Set<string>();\n updatedEditor.state.doc.descendants((node) => {\n if (\n node.type.name === \"mention\" &&\n node.attrs.type === \"asset\" &&\n typeof node.attrs.id === \"string\"\n ) {\n mentionedAssetIds.add(node.attrs.id);\n }\n });\n removeImagePreviews(\n (preview) =>\n preview.assetId !== undefined &&\n !mentionedAssetIds.has(preview.assetId),\n );\n }\n },\n immediatelyRender: false,\n });\n\n // Keep ref in sync so handleSubmit always has the editor\n editorRef.current = editor;\n\n useEffect(() => {\n editor?.setEditable(!disabled);\n }, [disabled, editor]);\n\n // This rich editor replaces ComposerPrimitive.Input, so it must mirror its\n // draft into assistant-ui's composer state. Statewire restores that state\n // when a send is rejected during reconnect, and a host may write to it\n // (`useAppendToComposer`); mirroring it back into Tiptap — parsed as the\n // Markdown it was serialized to, mention links included — preserves the\n // user's message instead of leaving an empty composer. Our own echo is\n // skipped on the ref; a restore's re-serialization may differ from the\n // value byte-for-byte, so the ref takes that form to stop a second pass.\n useEffect(() => {\n if (!editor) return;\n const value = composerInput.value;\n if (value === lastEmittedMarkdownRef.current) return;\n const currentMarkdown = serializeDraft(editor);\n lastEmittedMarkdownRef.current = currentMarkdown;\n if (value === currentMarkdown) return;\n // React node views flush their mounts synchronously. Restore outside\n // React's effect phase, and cancel a restore superseded by a newer draft.\n let cancelled = false;\n queueMicrotask(() => {\n if (cancelled || editor.isDestroyed) return;\n applyingRuntimeTextRef.current = true;\n try {\n editor.commands.setContent(value);\n lastEmittedMarkdownRef.current = serializeDraft(editor);\n } finally {\n applyingRuntimeTextRef.current = false;\n }\n onDraftStateChange?.(value.trim().length > 0);\n // A host prefill (`useAppendToComposer(..., { focus: true })`) asked\n // for the caret after its text: now that the text is on screen.\n if (handoff?.focusOnNextMirror) {\n handoff.focusOnNextMirror = false;\n editor.commands.focus(\"end\");\n }\n });\n return () => { cancelled = true; };\n }, [composerInput.value, editor, handoff, onDraftStateChange]);\n\n // Lets host UI outside the chat place the caret at the end of the draft\n // (`useAppendToComposer` focus option) without reaching into the DOM.\n useEffect(() => {\n if (!editor || !handoff) return;\n handoff.focusEnd = () => {\n if (!editor.isDestroyed) editor.commands.focus(\"end\");\n };\n return () => {\n handoff.focusEnd = null;\n };\n }, [editor, handoff]);\n\n const insertAssetMention = useCallback(\n (asset: UploadedChatAsset, sourceFile?: File) => {\n const currentEditor = editorRef.current;\n if (!currentEditor) throw new Error(\"The chat composer is not ready.\");\n currentEditor\n .chain()\n .focus()\n .insertContent({\n type: \"mention\",\n attrs: {\n type: \"asset\",\n name: asset.title,\n id: asset.id,\n assetType: asset.assetType,\n displayAsPreview:\n showImagePreviews && sourceFile?.type.startsWith(\"image/\"),\n },\n })\n // A text node, not a string: the Markdown extension parses string\n // content as Markdown, which would drop a lone space.\n .insertContent({ type: \"text\", text: \" \" })\n .run();\n\n if (\n showImagePreviews &&\n sourceFile?.type.startsWith(\"image/\") &&\n typeof URL.createObjectURL === \"function\"\n ) {\n const existing = imagePreviewsRef.current.find(\n (preview) => preview.file === sourceFile,\n );\n if (existing) {\n replaceImagePreviews(\n imagePreviewsRef.current.map((preview) =>\n preview.key === existing.key\n ? { ...preview, assetId: asset.id, name: asset.title }\n : preview,\n ),\n );\n } else {\n replaceImagePreviews([\n ...imagePreviewsRef.current,\n {\n key: crypto.randomUUID(),\n file: sourceFile,\n name: asset.title,\n src: URL.createObjectURL(sourceFile),\n assetId: asset.id,\n },\n ]);\n }\n }\n },\n [replaceImagePreviews, showImagePreviews],\n );\n\n const removeImagePreview = useCallback(\n (preview: ComposerImagePreview) => {\n if (!preview.assetId) return;\n const currentEditor = editorRef.current;\n if (!currentEditor) return;\n const positions: Array<{ from: number; to: number }> = [];\n currentEditor.state.doc.descendants((node, position) => {\n if (\n node.type.name === \"mention\" &&\n node.attrs.type === \"asset\" &&\n node.attrs.id === preview.assetId\n ) {\n positions.push({ from: position, to: position + node.nodeSize });\n }\n });\n let transaction = currentEditor.state.tr;\n for (const position of positions.reverse()) {\n transaction = transaction.delete(position.from, position.to);\n }\n if (transaction.docChanged) currentEditor.view.dispatch(transaction);\n removeImagePreviews((candidate) => candidate.key === preview.key);\n currentEditor.commands.focus();\n },\n [removeImagePreviews],\n );\n\n useEffect(\n () => registerChatAssetMentionHandler(threadId, insertAssetMention),\n [insertAssetMention, threadId],\n );\n\n const openMentionMenu = useCallback(() => {\n if (disabled) return;\n const currentEditor = editorRef.current;\n if (!currentEditor || currentEditor.isDestroyed) return;\n if (popupOpenRef.current) {\n currentEditor.commands.focus();\n return;\n }\n // The suggestion plugin only opens on an `@` that starts a word\n // (`allowedPrefixes`), so a trigger typed right after text — or after a\n // mention chip, which `textBetween` reports as its leaf text — gets the\n // separating space a user would type.\n const { from } = currentEditor.state.selection;\n const before =\n from > 0\n ? currentEditor.state.doc.textBetween(from - 1, from, \"\\0\", \"\\0\")\n : \"\";\n const needsSpace = before.length > 0 && !/[\\s([{\"']/.test(before);\n currentEditor\n .chain()\n .focus()\n .insertContent({ type: \"text\", text: needsSpace ? \" @\" : \"@\" })\n .run();\n }, [disabled]);\n\n useImperativeHandle(\n outerRef,\n () => ({\n submit: handleSubmit,\n addAttachment,\n insertAssetMention,\n openMentionMenu,\n }),\n [addAttachment, handleSubmit, insertAssetMention, openMentionMenu],\n );\n\n return (\n <>\n <style>{MENTION_STYLES}</style>\n <div\n className={cn(\n \"athena-chat-composer flex-1 min-w-0\",\n appearance === \"paper\" && \"athena-chat-composer--paper\",\n showImagePreviews && \"image-preview-composer\",\n )}\n aria-label=\"Message composer\"\n aria-disabled={disabled}\n onDragOver={(event) => {\n if (Array.from(event.dataTransfer.types).includes(\"Files\")) {\n event.preventDefault();\n event.dataTransfer.dropEffect = \"copy\";\n }\n }}\n onDrop={handleComposerDrop}\n >\n <ImagePreviewStrip\n previews={imagePreviews}\n onRemove={removeImagePreview}\n />\n <EditorContent editor={editor} />\n </div>\n\n {/* Olympus-style mention popup */}\n {popupOpen && popupProps && editor && (\n <MentionSuggestionPopup clientRect={popupProps.clientRect}>\n <MentionSuggestionList\n ref={listRef}\n store={store}\n editor={editor}\n query={popupProps.query}\n command={(item: MenuItem) => popupProps.command(item)}\n closeMenu={() => {\n setPopupOpen(false);\n editor.commands.focus();\n }}\n />\n </MentionSuggestionPopup>\n )}\n </>\n );\n },\n);\n\nTiptapComposer.displayName = \"TiptapComposer\";\n"],"names":["Extension","jsx","jsxs","LoaderCircleIcon","XIcon","forwardRef","useAui","unstable_useComposerInput","useComposerHandoffStore","useRef","PendingAttachmentUploads","useState","useCallback","useEffect","useMentionSuggestions","extractPastedImages","clipboardHasMeaningfulText","athenaDiagnostics","useEditor","createChatTextExtensions","MentionSuggestionsExtension","cn","CHAT_TEXT_CLASS","registerChatAssetMentionHandler","useImperativeHandle","Fragment","EditorContent","MentionSuggestionPopup","MentionSuggestionList"],"mappings":"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"}
@@ -6,6 +6,7 @@ import { useEditor, EditorContent } from "@tiptap/react";
6
6
  import Placeholder from "@tiptap/extension-placeholder";
7
7
  import { Extension } from "@tiptap/core";
8
8
  import { useAui, unstable_useComposerInput } from "@assistant-ui/react";
9
+ import { useComposerHandoffStore } from "../../composer/composer-handoff.js";
9
10
  import { LoaderCircleIcon, XIcon } from "lucide-react";
10
11
  import { CHAT_TEXT_CLASS, createChatTextExtensions } from "../rich-text/chat-text-schema.js";
11
12
  import { MentionSuggestionsExtension } from "../mentions/suggestions/mention-suggestions.extension.js";
@@ -164,6 +165,7 @@ const TiptapComposer = forwardRef(
164
165
  }, outerRef) => {
165
166
  const aui = useAui();
166
167
  const composerInput = unstable_useComposerInput();
168
+ const handoff = useComposerHandoffStore();
167
169
  const applyingRuntimeTextRef = useRef(false);
168
170
  const lastEmittedMarkdownRef = useRef(null);
169
171
  const listRef = useRef(null);
@@ -435,11 +437,24 @@ const TiptapComposer = forwardRef(
435
437
  applyingRuntimeTextRef.current = false;
436
438
  }
437
439
  onDraftStateChange == null ? void 0 : onDraftStateChange(value.trim().length > 0);
440
+ if (handoff == null ? void 0 : handoff.focusOnNextMirror) {
441
+ handoff.focusOnNextMirror = false;
442
+ editor.commands.focus("end");
443
+ }
438
444
  });
439
445
  return () => {
440
446
  cancelled = true;
441
447
  };
442
- }, [composerInput.value, editor, onDraftStateChange]);
448
+ }, [composerInput.value, editor, handoff, onDraftStateChange]);
449
+ useEffect(() => {
450
+ if (!editor || !handoff) return;
451
+ handoff.focusEnd = () => {
452
+ if (!editor.isDestroyed) editor.commands.focus("end");
453
+ };
454
+ return () => {
455
+ handoff.focusEnd = null;
456
+ };
457
+ }, [editor, handoff]);
443
458
  const insertAssetMention = useCallback(
444
459
  (asset, sourceFile) => {
445
460
  const currentEditor = editorRef.current;