@athenaintel/react 0.60.1 → 0.61.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +157 -5
- package/dist/agent-ui/AgentTarget.cjs +11 -0
- package/dist/agent-ui/AgentTarget.cjs.map +1 -0
- package/dist/agent-ui/AgentTarget.d.ts +17 -0
- package/dist/agent-ui/AgentTarget.js +11 -0
- package/dist/agent-ui/AgentTarget.js.map +1 -0
- package/dist/agent-ui/AgentUiLayer.cjs +387 -0
- package/dist/agent-ui/AgentUiLayer.cjs.map +1 -0
- package/dist/agent-ui/AgentUiLayer.d.ts +40 -0
- package/dist/agent-ui/AgentUiLayer.js +387 -0
- package/dist/agent-ui/AgentUiLayer.js.map +1 -0
- package/dist/agent-ui/contract.cjs +104 -0
- package/dist/agent-ui/contract.cjs.map +1 -0
- package/dist/agent-ui/contract.d.ts +113 -0
- package/dist/agent-ui/contract.js +104 -0
- package/dist/agent-ui/contract.js.map +1 -0
- package/dist/agent-ui/driver.cjs +335 -0
- package/dist/agent-ui/driver.cjs.map +1 -0
- package/dist/agent-ui/driver.d.ts +66 -0
- package/dist/agent-ui/driver.js +335 -0
- package/dist/agent-ui/driver.js.map +1 -0
- package/dist/agent-ui/index.cjs +33 -0
- package/dist/agent-ui/index.cjs.map +1 -0
- package/dist/agent-ui/index.d.ts +19 -0
- package/dist/agent-ui/index.js +33 -0
- package/dist/agent-ui/index.js.map +1 -0
- package/dist/agent-ui/pace.cjs +121 -0
- package/dist/agent-ui/pace.cjs.map +1 -0
- package/dist/agent-ui/pace.d.ts +66 -0
- package/dist/agent-ui/pace.js +121 -0
- package/dist/agent-ui/pace.js.map +1 -0
- package/dist/agent-ui/screen.cjs +471 -0
- package/dist/agent-ui/screen.cjs.map +1 -0
- package/dist/agent-ui/screen.d.ts +97 -0
- package/dist/agent-ui/screen.js +471 -0
- package/dist/agent-ui/screen.js.map +1 -0
- package/dist/agent-ui/secret-fields.cjs +50 -0
- package/dist/agent-ui/secret-fields.cjs.map +1 -0
- package/dist/agent-ui/secret-fields.d.ts +15 -0
- package/dist/agent-ui/secret-fields.js +50 -0
- package/dist/agent-ui/secret-fields.js.map +1 -0
- package/dist/agent-ui/stage.cjs +196 -0
- package/dist/agent-ui/stage.cjs.map +1 -0
- package/dist/agent-ui/stage.d.ts +110 -0
- package/dist/agent-ui/stage.js +196 -0
- package/dist/agent-ui/stage.js.map +1 -0
- package/dist/agent-ui/tools.cjs +448 -0
- package/dist/agent-ui/tools.cjs.map +1 -0
- package/dist/agent-ui/tools.d.ts +103 -0
- package/dist/agent-ui/tools.js +448 -0
- package/dist/agent-ui/tools.js.map +1 -0
- package/dist/agent-ui/use-agent-target.cjs +63 -0
- package/dist/agent-ui/use-agent-target.cjs.map +1 -0
- package/dist/agent-ui/use-agent-target.d.ts +27 -0
- package/dist/agent-ui/use-agent-target.js +63 -0
- package/dist/agent-ui/use-agent-target.js.map +1 -0
- package/dist/agent-ui/use-agent-ui.cjs +15 -0
- package/dist/agent-ui/use-agent-ui.cjs.map +1 -0
- package/dist/agent-ui/use-agent-ui.d.ts +24 -0
- package/dist/agent-ui/use-agent-ui.js +15 -0
- package/dist/agent-ui/use-agent-ui.js.map +1 -0
- package/dist/chat/AthenaChat.cjs +17 -4
- package/dist/chat/AthenaChat.cjs.map +1 -1
- package/dist/chat/AthenaChat.d.ts +13 -0
- package/dist/chat/AthenaChat.js +17 -4
- package/dist/chat/AthenaChat.js.map +1 -1
- package/dist/chat/StatewireClientToolBridge.cjs +17 -12
- package/dist/chat/StatewireClientToolBridge.cjs.map +1 -1
- package/dist/chat/StatewireClientToolBridge.d.ts +4 -2
- package/dist/chat/StatewireClientToolBridge.js +17 -12
- package/dist/chat/StatewireClientToolBridge.js.map +1 -1
- package/dist/chat/StatewireClientToolConsentCard.cjs +3 -2
- package/dist/chat/StatewireClientToolConsentCard.cjs.map +1 -1
- package/dist/chat/StatewireClientToolConsentCard.d.ts +2 -1
- package/dist/chat/StatewireClientToolConsentCard.js +3 -2
- package/dist/chat/StatewireClientToolConsentCard.js.map +1 -1
- package/dist/chat/chat-voice-controls.cjs +5 -2
- package/dist/chat/chat-voice-controls.cjs.map +1 -1
- package/dist/chat/chat-voice-controls.d.ts +6 -1
- package/dist/chat/chat-voice-controls.js +6 -3
- package/dist/chat/chat-voice-controls.js.map +1 -1
- package/dist/chat/client-tool-write-consent.cjs +16 -5
- package/dist/chat/client-tool-write-consent.cjs.map +1 -1
- package/dist/chat/client-tool-write-consent.d.ts +12 -4
- package/dist/chat/client-tool-write-consent.js +16 -5
- package/dist/chat/client-tool-write-consent.js.map +1 -1
- package/dist/chat-ui/messages/message-components.cjs +1 -1
- package/dist/chat-ui/messages/message-components.cjs.map +1 -1
- package/dist/chat-ui/messages/message-components.d.ts +2 -2
- package/dist/chat-ui/messages/message-components.js +1 -1
- package/dist/chat-ui/messages/message-components.js.map +1 -1
- package/dist/index.cjs +7 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/provider/AthenaProvider.cjs +37 -8
- package/dist/provider/AthenaProvider.cjs.map +1 -1
- package/dist/provider/AthenaProvider.d.ts +18 -1
- package/dist/provider/AthenaProvider.js +33 -4
- package/dist/provider/AthenaProvider.js.map +1 -1
- package/dist/react/package.json.cjs +1 -1
- package/dist/react/package.json.js +1 -1
- package/dist/styles.css +1 -1
- package/package.json +6 -1
|
@@ -11,6 +11,7 @@ function StatewireClientToolBridge({
|
|
|
11
11
|
tools,
|
|
12
12
|
threadId,
|
|
13
13
|
writeConsentSummaries,
|
|
14
|
+
writeConsentAgentName,
|
|
14
15
|
confirmWrites: hostConfirmWrites
|
|
15
16
|
}) {
|
|
16
17
|
const sendCommand = reactStatewire.useStatewireSendCommand();
|
|
@@ -42,6 +43,8 @@ function StatewireClientToolBridge({
|
|
|
42
43
|
);
|
|
43
44
|
const summariesRef = require$$0.useRef(writeConsentSummaries);
|
|
44
45
|
summariesRef.current = writeConsentSummaries;
|
|
46
|
+
const agentNameRef = require$$0.useRef(writeConsentAgentName);
|
|
47
|
+
agentNameRef.current = writeConsentAgentName;
|
|
45
48
|
const hostConfirmRef = require$$0.useRef(hostConfirmWrites);
|
|
46
49
|
hostConfirmRef.current = hostConfirmWrites;
|
|
47
50
|
const confirmWrites = require$$0.useCallback(
|
|
@@ -49,16 +52,18 @@ function StatewireClientToolBridge({
|
|
|
49
52
|
requestId,
|
|
50
53
|
writeToolNames
|
|
51
54
|
}) => {
|
|
52
|
-
var _a, _b;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
(name) => {
|
|
56
|
-
var _a2;
|
|
57
|
-
const summary = (_a2 = summariesRef.current) == null ? void 0 : _a2[name];
|
|
58
|
-
return summary ? { name, summary } : { name };
|
|
59
|
-
}
|
|
55
|
+
var _a, _b, _c;
|
|
56
|
+
const unanswered = Array.from(new Set(writeToolNames)).filter(
|
|
57
|
+
(toolName) => !(consentStore == null ? void 0 : consentStore.isGranted({ threadId, toolName }))
|
|
60
58
|
);
|
|
61
|
-
|
|
59
|
+
if (consentStore && unanswered.length === 0) return "proceed";
|
|
60
|
+
const consentTools = unanswered.map((name) => {
|
|
61
|
+
var _a2;
|
|
62
|
+
const summary = (_a2 = summariesRef.current) == null ? void 0 : _a2[name];
|
|
63
|
+
return summary ? { name, summary } : { name };
|
|
64
|
+
});
|
|
65
|
+
const agentName = (_a = agentNameRef.current) == null ? void 0 : _a.trim();
|
|
66
|
+
const request = agentName ? { requestId, threadId, tools: consentTools, agentName } : { requestId, threadId, tools: consentTools };
|
|
62
67
|
const hostConfirm = hostConfirmRef.current;
|
|
63
68
|
if (hostConfirm) {
|
|
64
69
|
const controller = new AbortController();
|
|
@@ -71,15 +76,15 @@ function StatewireClientToolBridge({
|
|
|
71
76
|
() => false
|
|
72
77
|
);
|
|
73
78
|
const outcome = await Promise.race([answered, abandoned]);
|
|
74
|
-
if (((
|
|
79
|
+
if (((_b = askingRef.current) == null ? void 0 : _b.requestId) === requestId) askingRef.current = null;
|
|
75
80
|
if (outcome === "abandoned" || controller.signal.aborted) return "abandoned";
|
|
76
|
-
if (outcome) consentStore == null ? void 0 : consentStore.grant(threadId);
|
|
81
|
+
if (outcome) consentStore == null ? void 0 : consentStore.grant({ threadId, toolNames: unanswered });
|
|
77
82
|
return outcome ? "proceed" : "declined";
|
|
78
83
|
}
|
|
79
84
|
if (!consentStore) return "declined";
|
|
80
85
|
askingRef.current = { requestId, abandon: () => consentStore.abandon(requestId) };
|
|
81
86
|
const decision = await consentStore.ask(request);
|
|
82
|
-
if (((
|
|
87
|
+
if (((_c = askingRef.current) == null ? void 0 : _c.requestId) === requestId) askingRef.current = null;
|
|
83
88
|
return decision;
|
|
84
89
|
},
|
|
85
90
|
[consentStore, threadId]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StatewireClientToolBridge.cjs","sources":["../../src/chat/StatewireClientToolBridge.tsx"],"sourcesContent":["import { useStatewireRuns, useStatewireSendCommand } from '@assistant-ui/react-statewire';\nimport {\n type ClientToolBridgeTool,\n type ClientToolWriteConsentDecision,\n createClientToolRequestTracker,\n type RegisteredClientTool,\n useStatewireClientToolBridge,\n} from '@athenaintel/agent-runtime';\nimport type { ReadonlyJSONObject } from 'assistant-stream/utils';\nimport { useCallback, useContext, useEffect, useMemo, useRef } from 'react';\nimport {\n type ClientToolWriteConfirmer,\n ClientToolWriteConsentContext,\n type ClientToolWriteConsentTool,\n} from './client-tool-write-consent';\n\n// Shared across every mount: the same thread can be attached from two mounted\n// surfaces, and a parked client-tool call must be executed and resumed by\n// exactly one of them. Whichever bridge claims the request first owns it.\nconst clientToolRequests = createClientToolRequestTracker();\n\n/** Answers the model for a write the person did not allow. */\nexport const SDK_CLIENT_TOOL_WRITE_DECLINED_MESSAGE =\n 'The person did not allow this. Do not retry it; ask them how they want to proceed.';\n\n/**\n * Executes the deep agent's frontend tool calls in statewire mode.\n *\n * Tools declared through `runConfig.custom.client_tools` have no backend\n * implementation: a call parks the run and surfaces here as a statewire\n * input request. The bridge runs the matching local handler and resumes the\n * run with a per-call result envelope, so a missing tool or a thrown handler\n * still frees the run with an error the model can act on instead of hanging.\n *\n * A batch holding a tool marked `requiresWriteConsent` first asks the person\n * (once per conversation): through the host's `confirmWrites` when it gave\n * one, else through the consent store the nearest `AthenaProvider` holds,\n * which `StatewireClientToolConsentCard` draws. With neither there is nobody\n * to ask, so the writes are declined.\n *\n * The claim lifecycle, liveness guards, and wire-safety live in\n * `@athenaintel/agent-runtime`'s `useStatewireClientToolBridge`; this wrapper\n * only adapts the shared `RegisteredClientTool` shape and the SDK's transport.\n */\nexport function StatewireClientToolBridge({\n tools,\n threadId,\n writeConsentSummaries,\n confirmWrites: hostConfirmWrites,\n}: {\n tools: readonly RegisteredClientTool[];\n threadId: string;\n /** Each write tool's own words for what it does, by tool name. */\n writeConsentSummaries?: Readonly<Record<string, string>>;\n /** The host's own consent UI, in place of the SDK's card. */\n confirmWrites?: ClientToolWriteConfirmer;\n}): null {\n const sendCommand = useStatewireSendCommand();\n const inputRequests = useStatewireRuns().inputRequests;\n const consentStore = useContext(ClientToolWriteConsentContext);\n\n const bridgeTools = useMemo<readonly ClientToolBridgeTool[]>(\n () =>\n tools.map((tool) => ({\n name: tool.definition.name,\n requiresWrite: tool.definition.requiresWrite,\n run: (args, context) => tool.handler(args, context),\n })),\n [tools],\n );\n\n // The question on screen belongs to one parked request. When that request\n // goes away (stop, an answer from elsewhere, another conversation), close\n // the question instead of leaving it up for a call that no longer exists.\n const askingRef = useRef<{ requestId: string; abandon: () => void } | null>(null);\n useEffect(() => {\n const asking = askingRef.current;\n if (!asking) return;\n if ((inputRequests ?? []).some((request) => request.id === asking.requestId)) return;\n askingRef.current = null;\n asking.abandon();\n }, [inputRequests]);\n useEffect(\n () => () => {\n askingRef.current?.abandon();\n askingRef.current = null;\n },\n [],\n );\n\n const summariesRef = useRef(writeConsentSummaries);\n summariesRef.current = writeConsentSummaries;\n const hostConfirmRef = useRef(hostConfirmWrites);\n hostConfirmRef.current = hostConfirmWrites;\n\n const confirmWrites = useCallback(\n async ({\n requestId,\n writeToolNames,\n }: {\n requestId: string;\n writeToolNames: readonly string[];\n }): Promise<ClientToolWriteConsentDecision> => {\n if (consentStore?.isGranted(threadId)) return 'proceed';\n const consentTools: ClientToolWriteConsentTool[] = Array.from(new Set(writeToolNames)).map(\n (name) => {\n const summary = summariesRef.current?.[name];\n return summary ? { name, summary } : { name };\n },\n );\n const request = { requestId, threadId, tools: consentTools };\n\n const hostConfirm = hostConfirmRef.current;\n if (hostConfirm) {\n const controller = new AbortController();\n askingRef.current = { requestId, abandon: () => controller.abort() };\n // The question ends when the person answers or when its call goes\n // away, whichever is first: a host dialog that closes on the signal\n // without settling its promise must not hold the request's claim.\n const abandoned = new Promise<'abandoned'>((resolve) => {\n controller.signal.addEventListener('abort', () => resolve('abandoned'), { once: true });\n });\n const answered = Promise.resolve()\n .then(() => hostConfirm({ ...request, signal: controller.signal }))\n // A consent UI that failed is not a yes.\n .then(\n (allowed) => allowed === true,\n () => false,\n );\n const outcome = await Promise.race([answered, abandoned]);\n if (askingRef.current?.requestId === requestId) askingRef.current = null;\n // An answer that arrives after the call went away is discarded.\n if (outcome === 'abandoned' || controller.signal.aborted) return 'abandoned';\n if (outcome) consentStore?.grant(threadId);\n return outcome ? 'proceed' : 'declined';\n }\n\n if (!consentStore) return 'declined';\n askingRef.current = { requestId, abandon: () => consentStore.abandon(requestId) };\n const decision = await consentStore.ask(request);\n if (askingRef.current?.requestId === requestId) askingRef.current = null;\n return decision;\n },\n [consentStore, threadId],\n );\n\n useStatewireClientToolBridge({\n threadId,\n tools: bridgeTools,\n tracker: clientToolRequests,\n inputRequests,\n confirmWrites,\n writeDeclinedMessage: SDK_CLIENT_TOOL_WRITE_DECLINED_MESSAGE,\n sendResume: (requestId, value) => {\n // Every envelope value is already wire-safe (per-call JSON round-trip in\n // the shared batch engine), so the batch only needs the transport's\n // JSON typing.\n return sendCommand({\n type: 'run/input',\n requestId,\n response: { type: 'resume', value: value as ReadonlyJSONObject },\n });\n },\n });\n\n return null;\n}\n"],"names":["createClientToolRequestTracker","useStatewireSendCommand","useStatewireRuns","useContext","ClientToolWriteConsentContext","useMemo","useRef","useEffect","useCallback","_a","useStatewireClientToolBridge"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"StatewireClientToolBridge.cjs","sources":["../../src/chat/StatewireClientToolBridge.tsx"],"sourcesContent":["import { useStatewireRuns, useStatewireSendCommand } from '@assistant-ui/react-statewire';\nimport {\n type ClientToolBridgeTool,\n type ClientToolWriteConsentDecision,\n createClientToolRequestTracker,\n type RegisteredClientTool,\n useStatewireClientToolBridge,\n} from '@athenaintel/agent-runtime';\nimport type { ReadonlyJSONObject } from 'assistant-stream/utils';\nimport { useCallback, useContext, useEffect, useMemo, useRef } from 'react';\nimport {\n type ClientToolWriteConfirmer,\n ClientToolWriteConsentContext,\n type ClientToolWriteConsentRequest,\n type ClientToolWriteConsentTool,\n} from './client-tool-write-consent';\n\n// Shared across every mount: the same thread can be attached from two mounted\n// surfaces, and a parked client-tool call must be executed and resumed by\n// exactly one of them. Whichever bridge claims the request first owns it.\nconst clientToolRequests = createClientToolRequestTracker();\n\n/** Answers the model for a write the person did not allow. */\nexport const SDK_CLIENT_TOOL_WRITE_DECLINED_MESSAGE =\n 'The person did not allow this. Do not retry it; ask them how they want to proceed.';\n\n/**\n * Executes the deep agent's frontend tool calls in statewire mode.\n *\n * Tools declared through `runConfig.custom.client_tools` have no backend\n * implementation: a call parks the run and surfaces here as a statewire\n * input request. The bridge runs the matching local handler and resumes the\n * run with a per-call result envelope, so a missing tool or a thrown handler\n * still frees the run with an error the model can act on instead of hanging.\n *\n * A batch holding a tool marked `requiresWriteConsent` first asks the person\n * (once per tool and conversation): through the host's `confirmWrites` when it gave\n * one, else through the consent store the nearest `AthenaProvider` holds,\n * which `StatewireClientToolConsentCard` draws. With neither there is nobody\n * to ask, so the writes are declined.\n *\n * The claim lifecycle, liveness guards, and wire-safety live in\n * `@athenaintel/agent-runtime`'s `useStatewireClientToolBridge`; this wrapper\n * only adapts the shared `RegisteredClientTool` shape and the SDK's transport.\n */\nexport function StatewireClientToolBridge({\n tools,\n threadId,\n writeConsentSummaries,\n writeConsentAgentName,\n confirmWrites: hostConfirmWrites,\n}: {\n tools: readonly RegisteredClientTool[];\n threadId: string;\n /** Each write tool's own words for what it does, by tool name. */\n writeConsentSummaries?: Readonly<Record<string, string>>;\n /** The agent's name as the host gave it; the question then names who is asking. */\n writeConsentAgentName?: string;\n /** The host's own consent UI, in place of the SDK's card. */\n confirmWrites?: ClientToolWriteConfirmer;\n}): null {\n const sendCommand = useStatewireSendCommand();\n const inputRequests = useStatewireRuns().inputRequests;\n const consentStore = useContext(ClientToolWriteConsentContext);\n\n const bridgeTools = useMemo<readonly ClientToolBridgeTool[]>(\n () =>\n tools.map((tool) => ({\n name: tool.definition.name,\n requiresWrite: tool.definition.requiresWrite,\n run: (args, context) => tool.handler(args, context),\n })),\n [tools],\n );\n\n // The question on screen belongs to one parked request. When that request\n // goes away (stop, an answer from elsewhere, another conversation), close\n // the question instead of leaving it up for a call that no longer exists.\n const askingRef = useRef<{ requestId: string; abandon: () => void } | null>(null);\n useEffect(() => {\n const asking = askingRef.current;\n if (!asking) return;\n if ((inputRequests ?? []).some((request) => request.id === asking.requestId)) return;\n askingRef.current = null;\n asking.abandon();\n }, [inputRequests]);\n useEffect(\n () => () => {\n askingRef.current?.abandon();\n askingRef.current = null;\n },\n [],\n );\n\n const summariesRef = useRef(writeConsentSummaries);\n summariesRef.current = writeConsentSummaries;\n const agentNameRef = useRef(writeConsentAgentName);\n agentNameRef.current = writeConsentAgentName;\n const hostConfirmRef = useRef(hostConfirmWrites);\n hostConfirmRef.current = hostConfirmWrites;\n\n const confirmWrites = useCallback(\n async ({\n requestId,\n writeToolNames,\n }: {\n requestId: string;\n writeToolNames: readonly string[];\n }): Promise<ClientToolWriteConsentDecision> => {\n // Only the tools the person has not yet allowed in this conversation\n // are asked about: a yes to one tool never covers another.\n const unanswered = Array.from(new Set(writeToolNames)).filter(\n (toolName) => !consentStore?.isGranted({ threadId, toolName }),\n );\n if (consentStore && unanswered.length === 0) return 'proceed';\n const consentTools: ClientToolWriteConsentTool[] = unanswered.map((name) => {\n const summary = summariesRef.current?.[name];\n return summary ? { name, summary } : { name };\n });\n const agentName = agentNameRef.current?.trim();\n const request: ClientToolWriteConsentRequest = agentName\n ? { requestId, threadId, tools: consentTools, agentName }\n : { requestId, threadId, tools: consentTools };\n\n const hostConfirm = hostConfirmRef.current;\n if (hostConfirm) {\n const controller = new AbortController();\n askingRef.current = { requestId, abandon: () => controller.abort() };\n // The question ends when the person answers or when its call goes\n // away, whichever is first: a host dialog that closes on the signal\n // without settling its promise must not hold the request's claim.\n const abandoned = new Promise<'abandoned'>((resolve) => {\n controller.signal.addEventListener('abort', () => resolve('abandoned'), { once: true });\n });\n const answered = Promise.resolve()\n .then(() => hostConfirm({ ...request, signal: controller.signal }))\n // A consent UI that failed is not a yes.\n .then(\n (allowed) => allowed === true,\n () => false,\n );\n const outcome = await Promise.race([answered, abandoned]);\n if (askingRef.current?.requestId === requestId) askingRef.current = null;\n // An answer that arrives after the call went away is discarded.\n if (outcome === 'abandoned' || controller.signal.aborted) return 'abandoned';\n if (outcome) consentStore?.grant({ threadId, toolNames: unanswered });\n return outcome ? 'proceed' : 'declined';\n }\n\n if (!consentStore) return 'declined';\n askingRef.current = { requestId, abandon: () => consentStore.abandon(requestId) };\n const decision = await consentStore.ask(request);\n if (askingRef.current?.requestId === requestId) askingRef.current = null;\n return decision;\n },\n [consentStore, threadId],\n );\n\n useStatewireClientToolBridge({\n threadId,\n tools: bridgeTools,\n tracker: clientToolRequests,\n inputRequests,\n confirmWrites,\n writeDeclinedMessage: SDK_CLIENT_TOOL_WRITE_DECLINED_MESSAGE,\n sendResume: (requestId, value) => {\n // Every envelope value is already wire-safe (per-call JSON round-trip in\n // the shared batch engine), so the batch only needs the transport's\n // JSON typing.\n return sendCommand({\n type: 'run/input',\n requestId,\n response: { type: 'resume', value: value as ReadonlyJSONObject },\n });\n },\n });\n\n return null;\n}\n"],"names":["createClientToolRequestTracker","useStatewireSendCommand","useStatewireRuns","useContext","ClientToolWriteConsentContext","useMemo","useRef","useEffect","useCallback","_a","useStatewireClientToolBridge"],"mappings":"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"}
|
|
@@ -12,7 +12,7 @@ export declare const SDK_CLIENT_TOOL_WRITE_DECLINED_MESSAGE = "The person did no
|
|
|
12
12
|
* still frees the run with an error the model can act on instead of hanging.
|
|
13
13
|
*
|
|
14
14
|
* A batch holding a tool marked `requiresWriteConsent` first asks the person
|
|
15
|
-
* (once per conversation): through the host's `confirmWrites` when it gave
|
|
15
|
+
* (once per tool and conversation): through the host's `confirmWrites` when it gave
|
|
16
16
|
* one, else through the consent store the nearest `AthenaProvider` holds,
|
|
17
17
|
* which `StatewireClientToolConsentCard` draws. With neither there is nobody
|
|
18
18
|
* to ask, so the writes are declined.
|
|
@@ -21,11 +21,13 @@ export declare const SDK_CLIENT_TOOL_WRITE_DECLINED_MESSAGE = "The person did no
|
|
|
21
21
|
* `@athenaintel/agent-runtime`'s `useStatewireClientToolBridge`; this wrapper
|
|
22
22
|
* only adapts the shared `RegisteredClientTool` shape and the SDK's transport.
|
|
23
23
|
*/
|
|
24
|
-
export declare function StatewireClientToolBridge({ tools, threadId, writeConsentSummaries, confirmWrites: hostConfirmWrites, }: {
|
|
24
|
+
export declare function StatewireClientToolBridge({ tools, threadId, writeConsentSummaries, writeConsentAgentName, confirmWrites: hostConfirmWrites, }: {
|
|
25
25
|
tools: readonly RegisteredClientTool[];
|
|
26
26
|
threadId: string;
|
|
27
27
|
/** Each write tool's own words for what it does, by tool name. */
|
|
28
28
|
writeConsentSummaries?: Readonly<Record<string, string>>;
|
|
29
|
+
/** The agent's name as the host gave it; the question then names who is asking. */
|
|
30
|
+
writeConsentAgentName?: string;
|
|
29
31
|
/** The host's own consent UI, in place of the SDK's card. */
|
|
30
32
|
confirmWrites?: ClientToolWriteConfirmer;
|
|
31
33
|
}): null;
|
|
@@ -9,6 +9,7 @@ function StatewireClientToolBridge({
|
|
|
9
9
|
tools,
|
|
10
10
|
threadId,
|
|
11
11
|
writeConsentSummaries,
|
|
12
|
+
writeConsentAgentName,
|
|
12
13
|
confirmWrites: hostConfirmWrites
|
|
13
14
|
}) {
|
|
14
15
|
const sendCommand = useStatewireSendCommand();
|
|
@@ -40,6 +41,8 @@ function StatewireClientToolBridge({
|
|
|
40
41
|
);
|
|
41
42
|
const summariesRef = useRef(writeConsentSummaries);
|
|
42
43
|
summariesRef.current = writeConsentSummaries;
|
|
44
|
+
const agentNameRef = useRef(writeConsentAgentName);
|
|
45
|
+
agentNameRef.current = writeConsentAgentName;
|
|
43
46
|
const hostConfirmRef = useRef(hostConfirmWrites);
|
|
44
47
|
hostConfirmRef.current = hostConfirmWrites;
|
|
45
48
|
const confirmWrites = useCallback(
|
|
@@ -47,16 +50,18 @@ function StatewireClientToolBridge({
|
|
|
47
50
|
requestId,
|
|
48
51
|
writeToolNames
|
|
49
52
|
}) => {
|
|
50
|
-
var _a, _b;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
(name) => {
|
|
54
|
-
var _a2;
|
|
55
|
-
const summary = (_a2 = summariesRef.current) == null ? void 0 : _a2[name];
|
|
56
|
-
return summary ? { name, summary } : { name };
|
|
57
|
-
}
|
|
53
|
+
var _a, _b, _c;
|
|
54
|
+
const unanswered = Array.from(new Set(writeToolNames)).filter(
|
|
55
|
+
(toolName) => !(consentStore == null ? void 0 : consentStore.isGranted({ threadId, toolName }))
|
|
58
56
|
);
|
|
59
|
-
|
|
57
|
+
if (consentStore && unanswered.length === 0) return "proceed";
|
|
58
|
+
const consentTools = unanswered.map((name) => {
|
|
59
|
+
var _a2;
|
|
60
|
+
const summary = (_a2 = summariesRef.current) == null ? void 0 : _a2[name];
|
|
61
|
+
return summary ? { name, summary } : { name };
|
|
62
|
+
});
|
|
63
|
+
const agentName = (_a = agentNameRef.current) == null ? void 0 : _a.trim();
|
|
64
|
+
const request = agentName ? { requestId, threadId, tools: consentTools, agentName } : { requestId, threadId, tools: consentTools };
|
|
60
65
|
const hostConfirm = hostConfirmRef.current;
|
|
61
66
|
if (hostConfirm) {
|
|
62
67
|
const controller = new AbortController();
|
|
@@ -69,15 +74,15 @@ function StatewireClientToolBridge({
|
|
|
69
74
|
() => false
|
|
70
75
|
);
|
|
71
76
|
const outcome = await Promise.race([answered, abandoned]);
|
|
72
|
-
if (((
|
|
77
|
+
if (((_b = askingRef.current) == null ? void 0 : _b.requestId) === requestId) askingRef.current = null;
|
|
73
78
|
if (outcome === "abandoned" || controller.signal.aborted) return "abandoned";
|
|
74
|
-
if (outcome) consentStore == null ? void 0 : consentStore.grant(threadId);
|
|
79
|
+
if (outcome) consentStore == null ? void 0 : consentStore.grant({ threadId, toolNames: unanswered });
|
|
75
80
|
return outcome ? "proceed" : "declined";
|
|
76
81
|
}
|
|
77
82
|
if (!consentStore) return "declined";
|
|
78
83
|
askingRef.current = { requestId, abandon: () => consentStore.abandon(requestId) };
|
|
79
84
|
const decision = await consentStore.ask(request);
|
|
80
|
-
if (((
|
|
85
|
+
if (((_c = askingRef.current) == null ? void 0 : _c.requestId) === requestId) askingRef.current = null;
|
|
81
86
|
return decision;
|
|
82
87
|
},
|
|
83
88
|
[consentStore, threadId]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StatewireClientToolBridge.js","sources":["../../src/chat/StatewireClientToolBridge.tsx"],"sourcesContent":["import { useStatewireRuns, useStatewireSendCommand } from '@assistant-ui/react-statewire';\nimport {\n type ClientToolBridgeTool,\n type ClientToolWriteConsentDecision,\n createClientToolRequestTracker,\n type RegisteredClientTool,\n useStatewireClientToolBridge,\n} from '@athenaintel/agent-runtime';\nimport type { ReadonlyJSONObject } from 'assistant-stream/utils';\nimport { useCallback, useContext, useEffect, useMemo, useRef } from 'react';\nimport {\n type ClientToolWriteConfirmer,\n ClientToolWriteConsentContext,\n type ClientToolWriteConsentTool,\n} from './client-tool-write-consent';\n\n// Shared across every mount: the same thread can be attached from two mounted\n// surfaces, and a parked client-tool call must be executed and resumed by\n// exactly one of them. Whichever bridge claims the request first owns it.\nconst clientToolRequests = createClientToolRequestTracker();\n\n/** Answers the model for a write the person did not allow. */\nexport const SDK_CLIENT_TOOL_WRITE_DECLINED_MESSAGE =\n 'The person did not allow this. Do not retry it; ask them how they want to proceed.';\n\n/**\n * Executes the deep agent's frontend tool calls in statewire mode.\n *\n * Tools declared through `runConfig.custom.client_tools` have no backend\n * implementation: a call parks the run and surfaces here as a statewire\n * input request. The bridge runs the matching local handler and resumes the\n * run with a per-call result envelope, so a missing tool or a thrown handler\n * still frees the run with an error the model can act on instead of hanging.\n *\n * A batch holding a tool marked `requiresWriteConsent` first asks the person\n * (once per conversation): through the host's `confirmWrites` when it gave\n * one, else through the consent store the nearest `AthenaProvider` holds,\n * which `StatewireClientToolConsentCard` draws. With neither there is nobody\n * to ask, so the writes are declined.\n *\n * The claim lifecycle, liveness guards, and wire-safety live in\n * `@athenaintel/agent-runtime`'s `useStatewireClientToolBridge`; this wrapper\n * only adapts the shared `RegisteredClientTool` shape and the SDK's transport.\n */\nexport function StatewireClientToolBridge({\n tools,\n threadId,\n writeConsentSummaries,\n confirmWrites: hostConfirmWrites,\n}: {\n tools: readonly RegisteredClientTool[];\n threadId: string;\n /** Each write tool's own words for what it does, by tool name. */\n writeConsentSummaries?: Readonly<Record<string, string>>;\n /** The host's own consent UI, in place of the SDK's card. */\n confirmWrites?: ClientToolWriteConfirmer;\n}): null {\n const sendCommand = useStatewireSendCommand();\n const inputRequests = useStatewireRuns().inputRequests;\n const consentStore = useContext(ClientToolWriteConsentContext);\n\n const bridgeTools = useMemo<readonly ClientToolBridgeTool[]>(\n () =>\n tools.map((tool) => ({\n name: tool.definition.name,\n requiresWrite: tool.definition.requiresWrite,\n run: (args, context) => tool.handler(args, context),\n })),\n [tools],\n );\n\n // The question on screen belongs to one parked request. When that request\n // goes away (stop, an answer from elsewhere, another conversation), close\n // the question instead of leaving it up for a call that no longer exists.\n const askingRef = useRef<{ requestId: string; abandon: () => void } | null>(null);\n useEffect(() => {\n const asking = askingRef.current;\n if (!asking) return;\n if ((inputRequests ?? []).some((request) => request.id === asking.requestId)) return;\n askingRef.current = null;\n asking.abandon();\n }, [inputRequests]);\n useEffect(\n () => () => {\n askingRef.current?.abandon();\n askingRef.current = null;\n },\n [],\n );\n\n const summariesRef = useRef(writeConsentSummaries);\n summariesRef.current = writeConsentSummaries;\n const hostConfirmRef = useRef(hostConfirmWrites);\n hostConfirmRef.current = hostConfirmWrites;\n\n const confirmWrites = useCallback(\n async ({\n requestId,\n writeToolNames,\n }: {\n requestId: string;\n writeToolNames: readonly string[];\n }): Promise<ClientToolWriteConsentDecision> => {\n if (consentStore?.isGranted(threadId)) return 'proceed';\n const consentTools: ClientToolWriteConsentTool[] = Array.from(new Set(writeToolNames)).map(\n (name) => {\n const summary = summariesRef.current?.[name];\n return summary ? { name, summary } : { name };\n },\n );\n const request = { requestId, threadId, tools: consentTools };\n\n const hostConfirm = hostConfirmRef.current;\n if (hostConfirm) {\n const controller = new AbortController();\n askingRef.current = { requestId, abandon: () => controller.abort() };\n // The question ends when the person answers or when its call goes\n // away, whichever is first: a host dialog that closes on the signal\n // without settling its promise must not hold the request's claim.\n const abandoned = new Promise<'abandoned'>((resolve) => {\n controller.signal.addEventListener('abort', () => resolve('abandoned'), { once: true });\n });\n const answered = Promise.resolve()\n .then(() => hostConfirm({ ...request, signal: controller.signal }))\n // A consent UI that failed is not a yes.\n .then(\n (allowed) => allowed === true,\n () => false,\n );\n const outcome = await Promise.race([answered, abandoned]);\n if (askingRef.current?.requestId === requestId) askingRef.current = null;\n // An answer that arrives after the call went away is discarded.\n if (outcome === 'abandoned' || controller.signal.aborted) return 'abandoned';\n if (outcome) consentStore?.grant(threadId);\n return outcome ? 'proceed' : 'declined';\n }\n\n if (!consentStore) return 'declined';\n askingRef.current = { requestId, abandon: () => consentStore.abandon(requestId) };\n const decision = await consentStore.ask(request);\n if (askingRef.current?.requestId === requestId) askingRef.current = null;\n return decision;\n },\n [consentStore, threadId],\n );\n\n useStatewireClientToolBridge({\n threadId,\n tools: bridgeTools,\n tracker: clientToolRequests,\n inputRequests,\n confirmWrites,\n writeDeclinedMessage: SDK_CLIENT_TOOL_WRITE_DECLINED_MESSAGE,\n sendResume: (requestId, value) => {\n // Every envelope value is already wire-safe (per-call JSON round-trip in\n // the shared batch engine), so the batch only needs the transport's\n // JSON typing.\n return sendCommand({\n type: 'run/input',\n requestId,\n response: { type: 'resume', value: value as ReadonlyJSONObject },\n });\n },\n });\n\n return null;\n}\n"],"names":["_a"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"StatewireClientToolBridge.js","sources":["../../src/chat/StatewireClientToolBridge.tsx"],"sourcesContent":["import { useStatewireRuns, useStatewireSendCommand } from '@assistant-ui/react-statewire';\nimport {\n type ClientToolBridgeTool,\n type ClientToolWriteConsentDecision,\n createClientToolRequestTracker,\n type RegisteredClientTool,\n useStatewireClientToolBridge,\n} from '@athenaintel/agent-runtime';\nimport type { ReadonlyJSONObject } from 'assistant-stream/utils';\nimport { useCallback, useContext, useEffect, useMemo, useRef } from 'react';\nimport {\n type ClientToolWriteConfirmer,\n ClientToolWriteConsentContext,\n type ClientToolWriteConsentRequest,\n type ClientToolWriteConsentTool,\n} from './client-tool-write-consent';\n\n// Shared across every mount: the same thread can be attached from two mounted\n// surfaces, and a parked client-tool call must be executed and resumed by\n// exactly one of them. Whichever bridge claims the request first owns it.\nconst clientToolRequests = createClientToolRequestTracker();\n\n/** Answers the model for a write the person did not allow. */\nexport const SDK_CLIENT_TOOL_WRITE_DECLINED_MESSAGE =\n 'The person did not allow this. Do not retry it; ask them how they want to proceed.';\n\n/**\n * Executes the deep agent's frontend tool calls in statewire mode.\n *\n * Tools declared through `runConfig.custom.client_tools` have no backend\n * implementation: a call parks the run and surfaces here as a statewire\n * input request. The bridge runs the matching local handler and resumes the\n * run with a per-call result envelope, so a missing tool or a thrown handler\n * still frees the run with an error the model can act on instead of hanging.\n *\n * A batch holding a tool marked `requiresWriteConsent` first asks the person\n * (once per tool and conversation): through the host's `confirmWrites` when it gave\n * one, else through the consent store the nearest `AthenaProvider` holds,\n * which `StatewireClientToolConsentCard` draws. With neither there is nobody\n * to ask, so the writes are declined.\n *\n * The claim lifecycle, liveness guards, and wire-safety live in\n * `@athenaintel/agent-runtime`'s `useStatewireClientToolBridge`; this wrapper\n * only adapts the shared `RegisteredClientTool` shape and the SDK's transport.\n */\nexport function StatewireClientToolBridge({\n tools,\n threadId,\n writeConsentSummaries,\n writeConsentAgentName,\n confirmWrites: hostConfirmWrites,\n}: {\n tools: readonly RegisteredClientTool[];\n threadId: string;\n /** Each write tool's own words for what it does, by tool name. */\n writeConsentSummaries?: Readonly<Record<string, string>>;\n /** The agent's name as the host gave it; the question then names who is asking. */\n writeConsentAgentName?: string;\n /** The host's own consent UI, in place of the SDK's card. */\n confirmWrites?: ClientToolWriteConfirmer;\n}): null {\n const sendCommand = useStatewireSendCommand();\n const inputRequests = useStatewireRuns().inputRequests;\n const consentStore = useContext(ClientToolWriteConsentContext);\n\n const bridgeTools = useMemo<readonly ClientToolBridgeTool[]>(\n () =>\n tools.map((tool) => ({\n name: tool.definition.name,\n requiresWrite: tool.definition.requiresWrite,\n run: (args, context) => tool.handler(args, context),\n })),\n [tools],\n );\n\n // The question on screen belongs to one parked request. When that request\n // goes away (stop, an answer from elsewhere, another conversation), close\n // the question instead of leaving it up for a call that no longer exists.\n const askingRef = useRef<{ requestId: string; abandon: () => void } | null>(null);\n useEffect(() => {\n const asking = askingRef.current;\n if (!asking) return;\n if ((inputRequests ?? []).some((request) => request.id === asking.requestId)) return;\n askingRef.current = null;\n asking.abandon();\n }, [inputRequests]);\n useEffect(\n () => () => {\n askingRef.current?.abandon();\n askingRef.current = null;\n },\n [],\n );\n\n const summariesRef = useRef(writeConsentSummaries);\n summariesRef.current = writeConsentSummaries;\n const agentNameRef = useRef(writeConsentAgentName);\n agentNameRef.current = writeConsentAgentName;\n const hostConfirmRef = useRef(hostConfirmWrites);\n hostConfirmRef.current = hostConfirmWrites;\n\n const confirmWrites = useCallback(\n async ({\n requestId,\n writeToolNames,\n }: {\n requestId: string;\n writeToolNames: readonly string[];\n }): Promise<ClientToolWriteConsentDecision> => {\n // Only the tools the person has not yet allowed in this conversation\n // are asked about: a yes to one tool never covers another.\n const unanswered = Array.from(new Set(writeToolNames)).filter(\n (toolName) => !consentStore?.isGranted({ threadId, toolName }),\n );\n if (consentStore && unanswered.length === 0) return 'proceed';\n const consentTools: ClientToolWriteConsentTool[] = unanswered.map((name) => {\n const summary = summariesRef.current?.[name];\n return summary ? { name, summary } : { name };\n });\n const agentName = agentNameRef.current?.trim();\n const request: ClientToolWriteConsentRequest = agentName\n ? { requestId, threadId, tools: consentTools, agentName }\n : { requestId, threadId, tools: consentTools };\n\n const hostConfirm = hostConfirmRef.current;\n if (hostConfirm) {\n const controller = new AbortController();\n askingRef.current = { requestId, abandon: () => controller.abort() };\n // The question ends when the person answers or when its call goes\n // away, whichever is first: a host dialog that closes on the signal\n // without settling its promise must not hold the request's claim.\n const abandoned = new Promise<'abandoned'>((resolve) => {\n controller.signal.addEventListener('abort', () => resolve('abandoned'), { once: true });\n });\n const answered = Promise.resolve()\n .then(() => hostConfirm({ ...request, signal: controller.signal }))\n // A consent UI that failed is not a yes.\n .then(\n (allowed) => allowed === true,\n () => false,\n );\n const outcome = await Promise.race([answered, abandoned]);\n if (askingRef.current?.requestId === requestId) askingRef.current = null;\n // An answer that arrives after the call went away is discarded.\n if (outcome === 'abandoned' || controller.signal.aborted) return 'abandoned';\n if (outcome) consentStore?.grant({ threadId, toolNames: unanswered });\n return outcome ? 'proceed' : 'declined';\n }\n\n if (!consentStore) return 'declined';\n askingRef.current = { requestId, abandon: () => consentStore.abandon(requestId) };\n const decision = await consentStore.ask(request);\n if (askingRef.current?.requestId === requestId) askingRef.current = null;\n return decision;\n },\n [consentStore, threadId],\n );\n\n useStatewireClientToolBridge({\n threadId,\n tools: bridgeTools,\n tracker: clientToolRequests,\n inputRequests,\n confirmWrites,\n writeDeclinedMessage: SDK_CLIENT_TOOL_WRITE_DECLINED_MESSAGE,\n sendResume: (requestId, value) => {\n // Every envelope value is already wire-safe (per-call JSON round-trip in\n // the shared batch engine), so the batch only needs the transport's\n // JSON typing.\n return sendCommand({\n type: 'run/input',\n requestId,\n response: { type: 'resume', value: value as ReadonlyJSONObject },\n });\n },\n });\n\n return null;\n}\n"],"names":["_a"],"mappings":"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"}
|
|
@@ -24,10 +24,11 @@ function joinWords(words) {
|
|
|
24
24
|
}
|
|
25
25
|
const StatewireClientToolConsentCard = ({
|
|
26
26
|
className,
|
|
27
|
-
agentName
|
|
27
|
+
agentName
|
|
28
28
|
}) => {
|
|
29
29
|
const { request, allow, decline } = clientToolWriteConsent.useClientToolWriteConsent();
|
|
30
30
|
if (!request) return null;
|
|
31
|
+
const asker = agentName ?? request.agentName ?? "The agent";
|
|
31
32
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
32
33
|
"section",
|
|
33
34
|
{
|
|
@@ -42,7 +43,7 @@ const StatewireClientToolConsentCard = ({
|
|
|
42
43
|
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.MousePointerClickIcon, { className: "mt-0.5 size-4 shrink-0 text-muted-foreground" }),
|
|
43
44
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
44
45
|
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-semibold text-sm", children: "Allow this?" }),
|
|
45
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "break-words pt-1 text-foreground text-sm leading-snug", children: clientToolConsentSentence({ agentName, request }) }),
|
|
46
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "break-words pt-1 text-foreground text-sm leading-snug", children: clientToolConsentSentence({ agentName: asker, request }) }),
|
|
46
47
|
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "pt-1 text-muted-foreground text-sm leading-snug", children: "Allowing covers the rest of this conversation." })
|
|
47
48
|
] })
|
|
48
49
|
] }),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StatewireClientToolConsentCard.cjs","sources":["../../src/chat/StatewireClientToolConsentCard.tsx"],"sourcesContent":["'use client';\n\nimport { CheckIcon, MousePointerClickIcon, XIcon } from 'lucide-react';\nimport type { FC } from 'react';\nimport { cn } from '../lib/utils';\nimport { Button } from '../ui/button';\nimport {\n type ClientToolWriteConsentRequest,\n useClientToolWriteConsent,\n} from './client-tool-write-consent';\n\nexport interface StatewireClientToolConsentCardProps {\n /** Applied to the card's section. */\n className?: string;\n /**\n * Who is asking, as the card names them (\"Jichard wants to …\"). Defaults to\n * \"The agent\".\n */\n agentName?: string;\n}\n\n/** What the batch does, in the card's sentence: its tools' own words, else their names. */\nexport function clientToolConsentSentence({\n agentName,\n request,\n}: {\n agentName: string;\n request: ClientToolWriteConsentRequest;\n}): string {\n const summaries = Array.from(\n new Set(request.tools.flatMap((tool) => (tool.summary ? [tool.summary] : []))),\n );\n if (summaries.length > 0 && summaries.length === request.tools.length) {\n return `${agentName} wants to ${joinWords(summaries)}.`;\n }\n const names = request.tools.map((tool) => tool.name);\n return `${agentName} wants to run ${joinWords(names)}, which ${names.length === 1 ? 'changes' : 'change'} something here.`;\n}\n\nfunction joinWords(words: readonly string[]): string {\n if (words.length <= 1) return words[0] ?? '';\n return `${words.slice(0, -1).join(', ')} and ${words[words.length - 1]}`;\n}\n\n/**\n * The question a frontend tool marked `requiresWriteConsent` puts to the\n * person before its first call in a conversation runs: allow (which covers\n * the rest of the conversation) or decline. `AthenaChat` renders it above the\n * composer; a host with its own chat mounts it there, or builds its own on\n * `useClientToolWriteConsent`. Renders nothing while no call is waiting.\n */\nexport const StatewireClientToolConsentCard: FC<StatewireClientToolConsentCardProps> = ({\n className,\n agentName
|
|
1
|
+
{"version":3,"file":"StatewireClientToolConsentCard.cjs","sources":["../../src/chat/StatewireClientToolConsentCard.tsx"],"sourcesContent":["'use client';\n\nimport { CheckIcon, MousePointerClickIcon, XIcon } from 'lucide-react';\nimport type { FC } from 'react';\nimport { cn } from '../lib/utils';\nimport { Button } from '../ui/button';\nimport {\n type ClientToolWriteConsentRequest,\n useClientToolWriteConsent,\n} from './client-tool-write-consent';\n\nexport interface StatewireClientToolConsentCardProps {\n /** Applied to the card's section. */\n className?: string;\n /**\n * Who is asking, as the card names them (\"Jichard wants to …\"). Defaults to\n * the name the question carries (`agentUi.agentName` on `AthenaProvider`),\n * else \"The agent\".\n */\n agentName?: string;\n}\n\n/** What the batch does, in the card's sentence: its tools' own words, else their names. */\nexport function clientToolConsentSentence({\n agentName,\n request,\n}: {\n agentName: string;\n request: ClientToolWriteConsentRequest;\n}): string {\n const summaries = Array.from(\n new Set(request.tools.flatMap((tool) => (tool.summary ? [tool.summary] : []))),\n );\n if (summaries.length > 0 && summaries.length === request.tools.length) {\n return `${agentName} wants to ${joinWords(summaries)}.`;\n }\n const names = request.tools.map((tool) => tool.name);\n return `${agentName} wants to run ${joinWords(names)}, which ${names.length === 1 ? 'changes' : 'change'} something here.`;\n}\n\nfunction joinWords(words: readonly string[]): string {\n if (words.length <= 1) return words[0] ?? '';\n return `${words.slice(0, -1).join(', ')} and ${words[words.length - 1]}`;\n}\n\n/**\n * The question a frontend tool marked `requiresWriteConsent` puts to the\n * person before its first call in a conversation runs: allow (which covers\n * the rest of the conversation) or decline. `AthenaChat` renders it above the\n * composer; a host with its own chat mounts it there, or builds its own on\n * `useClientToolWriteConsent`. Renders nothing while no call is waiting.\n */\nexport const StatewireClientToolConsentCard: FC<StatewireClientToolConsentCardProps> = ({\n className,\n agentName,\n}) => {\n const { request, allow, decline } = useClientToolWriteConsent();\n if (!request) return null;\n const asker = agentName ?? request.agentName ?? 'The agent';\n return (\n <section\n aria-label=\"Permission required\"\n data-testid=\"client-tool-consent-card\"\n className={cn(\n 'aui-client-tool-consent-card w-full rounded-2xl border border-border bg-card px-4 py-3 text-foreground shadow-xs',\n className,\n )}\n >\n <div className=\"flex items-start gap-3\">\n <MousePointerClickIcon className=\"mt-0.5 size-4 shrink-0 text-muted-foreground\" />\n <div className=\"min-w-0 flex-1\">\n <p className=\"font-semibold text-sm\">Allow this?</p>\n <p className=\"break-words pt-1 text-foreground text-sm leading-snug\">\n {clientToolConsentSentence({ agentName: asker, request })}\n </p>\n <p className=\"pt-1 text-muted-foreground text-sm leading-snug\">\n Allowing covers the rest of this conversation.\n </p>\n </div>\n </div>\n <div className=\"flex flex-wrap items-center gap-2 pt-3\">\n <Button size=\"sm\" onClick={allow}>\n <CheckIcon />\n Allow\n </Button>\n <Button size=\"sm\" variant=\"outline\" onClick={decline}>\n <XIcon />\n Decline\n </Button>\n </div>\n </section>\n );\n};\n"],"names":["useClientToolWriteConsent","jsxs","cn","jsx","MousePointerClickIcon","Button","CheckIcon","XIcon"],"mappings":";;;;;;;AAuBO,SAAS,0BAA0B;AAAA,EACxC;AAAA,EACA;AACF,GAGW;AACT,QAAM,YAAY,MAAM;AAAA,IACtB,IAAI,IAAI,QAAQ,MAAM,QAAQ,CAAC,SAAU,KAAK,UAAU,CAAC,KAAK,OAAO,IAAI,CAAA,CAAG,CAAC;AAAA,EAAA;AAE/E,MAAI,UAAU,SAAS,KAAK,UAAU,WAAW,QAAQ,MAAM,QAAQ;AACrE,WAAO,GAAG,SAAS,aAAa,UAAU,SAAS,CAAC;AAAA,EACtD;AACA,QAAM,QAAQ,QAAQ,MAAM,IAAI,CAAC,SAAS,KAAK,IAAI;AACnD,SAAO,GAAG,SAAS,iBAAiB,UAAU,KAAK,CAAC,WAAW,MAAM,WAAW,IAAI,YAAY,QAAQ;AAC1G;AAEA,SAAS,UAAU,OAAkC;AACnD,MAAI,MAAM,UAAU,EAAG,QAAO,MAAM,CAAC,KAAK;AAC1C,SAAO,GAAG,MAAM,MAAM,GAAG,EAAE,EAAE,KAAK,IAAI,CAAC,QAAQ,MAAM,MAAM,SAAS,CAAC,CAAC;AACxE;AASO,MAAM,iCAA0E,CAAC;AAAA,EACtF;AAAA,EACA;AACF,MAAM;AACJ,QAAM,EAAE,SAAS,OAAO,QAAA,IAAYA,uBAAAA,0BAAA;AACpC,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,QAAQ,aAAa,QAAQ,aAAa;AAChD,SACEC,2BAAAA;AAAAA,IAAC;AAAA,IAAA;AAAA,MACC,cAAW;AAAA,MACX,eAAY;AAAA,MACZ,WAAWC,OAAAA;AAAAA,QACT;AAAA,QACA;AAAA,MAAA;AAAA,MAGF,UAAA;AAAA,QAAAD,2BAAAA,KAAC,OAAA,EAAI,WAAU,0BACb,UAAA;AAAA,UAAAE,2BAAAA,IAACC,YAAAA,uBAAA,EAAsB,WAAU,+CAAA,CAA+C;AAAA,UAChFH,2BAAAA,KAAC,OAAA,EAAI,WAAU,kBACb,UAAA;AAAA,YAAAE,2BAAAA,IAAC,KAAA,EAAE,WAAU,yBAAwB,UAAA,eAAW;AAAA,YAChDA,2BAAAA,IAAC,KAAA,EAAE,WAAU,yDACV,UAAA,0BAA0B,EAAE,WAAW,OAAO,QAAA,CAAS,EAAA,CAC1D;AAAA,YACAA,2BAAAA,IAAC,KAAA,EAAE,WAAU,mDAAkD,UAAA,iDAAA,CAE/D;AAAA,UAAA,EAAA,CACF;AAAA,QAAA,GACF;AAAA,QACAF,2BAAAA,KAAC,OAAA,EAAI,WAAU,0CACb,UAAA;AAAA,UAAAA,2BAAAA,KAACI,OAAAA,QAAA,EAAO,MAAK,MAAK,SAAS,OACzB,UAAA;AAAA,YAAAF,2BAAAA,IAACG,YAAAA,WAAA,EAAU;AAAA,YAAE;AAAA,UAAA,GAEf;AAAA,0CACCD,OAAAA,QAAA,EAAO,MAAK,MAAK,SAAQ,WAAU,SAAS,SAC3C,UAAA;AAAA,YAAAF,2BAAAA,IAACI,YAAAA,OAAA,EAAM;AAAA,YAAE;AAAA,UAAA,EAAA,CAEX;AAAA,QAAA,EAAA,CACF;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGN;;;"}
|
|
@@ -5,7 +5,8 @@ export interface StatewireClientToolConsentCardProps {
|
|
|
5
5
|
className?: string;
|
|
6
6
|
/**
|
|
7
7
|
* Who is asking, as the card names them ("Jichard wants to …"). Defaults to
|
|
8
|
-
*
|
|
8
|
+
* the name the question carries (`agentUi.agentName` on `AthenaProvider`),
|
|
9
|
+
* else "The agent".
|
|
9
10
|
*/
|
|
10
11
|
agentName?: string;
|
|
11
12
|
}
|
|
@@ -22,10 +22,11 @@ function joinWords(words) {
|
|
|
22
22
|
}
|
|
23
23
|
const StatewireClientToolConsentCard = ({
|
|
24
24
|
className,
|
|
25
|
-
agentName
|
|
25
|
+
agentName
|
|
26
26
|
}) => {
|
|
27
27
|
const { request, allow, decline } = useClientToolWriteConsent();
|
|
28
28
|
if (!request) return null;
|
|
29
|
+
const asker = agentName ?? request.agentName ?? "The agent";
|
|
29
30
|
return /* @__PURE__ */ jsxs(
|
|
30
31
|
"section",
|
|
31
32
|
{
|
|
@@ -40,7 +41,7 @@ const StatewireClientToolConsentCard = ({
|
|
|
40
41
|
/* @__PURE__ */ jsx(MousePointerClickIcon, { className: "mt-0.5 size-4 shrink-0 text-muted-foreground" }),
|
|
41
42
|
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
42
43
|
/* @__PURE__ */ jsx("p", { className: "font-semibold text-sm", children: "Allow this?" }),
|
|
43
|
-
/* @__PURE__ */ jsx("p", { className: "break-words pt-1 text-foreground text-sm leading-snug", children: clientToolConsentSentence({ agentName, request }) }),
|
|
44
|
+
/* @__PURE__ */ jsx("p", { className: "break-words pt-1 text-foreground text-sm leading-snug", children: clientToolConsentSentence({ agentName: asker, request }) }),
|
|
44
45
|
/* @__PURE__ */ jsx("p", { className: "pt-1 text-muted-foreground text-sm leading-snug", children: "Allowing covers the rest of this conversation." })
|
|
45
46
|
] })
|
|
46
47
|
] }),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StatewireClientToolConsentCard.js","sources":["../../src/chat/StatewireClientToolConsentCard.tsx"],"sourcesContent":["'use client';\n\nimport { CheckIcon, MousePointerClickIcon, XIcon } from 'lucide-react';\nimport type { FC } from 'react';\nimport { cn } from '../lib/utils';\nimport { Button } from '../ui/button';\nimport {\n type ClientToolWriteConsentRequest,\n useClientToolWriteConsent,\n} from './client-tool-write-consent';\n\nexport interface StatewireClientToolConsentCardProps {\n /** Applied to the card's section. */\n className?: string;\n /**\n * Who is asking, as the card names them (\"Jichard wants to …\"). Defaults to\n * \"The agent\".\n */\n agentName?: string;\n}\n\n/** What the batch does, in the card's sentence: its tools' own words, else their names. */\nexport function clientToolConsentSentence({\n agentName,\n request,\n}: {\n agentName: string;\n request: ClientToolWriteConsentRequest;\n}): string {\n const summaries = Array.from(\n new Set(request.tools.flatMap((tool) => (tool.summary ? [tool.summary] : []))),\n );\n if (summaries.length > 0 && summaries.length === request.tools.length) {\n return `${agentName} wants to ${joinWords(summaries)}.`;\n }\n const names = request.tools.map((tool) => tool.name);\n return `${agentName} wants to run ${joinWords(names)}, which ${names.length === 1 ? 'changes' : 'change'} something here.`;\n}\n\nfunction joinWords(words: readonly string[]): string {\n if (words.length <= 1) return words[0] ?? '';\n return `${words.slice(0, -1).join(', ')} and ${words[words.length - 1]}`;\n}\n\n/**\n * The question a frontend tool marked `requiresWriteConsent` puts to the\n * person before its first call in a conversation runs: allow (which covers\n * the rest of the conversation) or decline. `AthenaChat` renders it above the\n * composer; a host with its own chat mounts it there, or builds its own on\n * `useClientToolWriteConsent`. Renders nothing while no call is waiting.\n */\nexport const StatewireClientToolConsentCard: FC<StatewireClientToolConsentCardProps> = ({\n className,\n agentName
|
|
1
|
+
{"version":3,"file":"StatewireClientToolConsentCard.js","sources":["../../src/chat/StatewireClientToolConsentCard.tsx"],"sourcesContent":["'use client';\n\nimport { CheckIcon, MousePointerClickIcon, XIcon } from 'lucide-react';\nimport type { FC } from 'react';\nimport { cn } from '../lib/utils';\nimport { Button } from '../ui/button';\nimport {\n type ClientToolWriteConsentRequest,\n useClientToolWriteConsent,\n} from './client-tool-write-consent';\n\nexport interface StatewireClientToolConsentCardProps {\n /** Applied to the card's section. */\n className?: string;\n /**\n * Who is asking, as the card names them (\"Jichard wants to …\"). Defaults to\n * the name the question carries (`agentUi.agentName` on `AthenaProvider`),\n * else \"The agent\".\n */\n agentName?: string;\n}\n\n/** What the batch does, in the card's sentence: its tools' own words, else their names. */\nexport function clientToolConsentSentence({\n agentName,\n request,\n}: {\n agentName: string;\n request: ClientToolWriteConsentRequest;\n}): string {\n const summaries = Array.from(\n new Set(request.tools.flatMap((tool) => (tool.summary ? [tool.summary] : []))),\n );\n if (summaries.length > 0 && summaries.length === request.tools.length) {\n return `${agentName} wants to ${joinWords(summaries)}.`;\n }\n const names = request.tools.map((tool) => tool.name);\n return `${agentName} wants to run ${joinWords(names)}, which ${names.length === 1 ? 'changes' : 'change'} something here.`;\n}\n\nfunction joinWords(words: readonly string[]): string {\n if (words.length <= 1) return words[0] ?? '';\n return `${words.slice(0, -1).join(', ')} and ${words[words.length - 1]}`;\n}\n\n/**\n * The question a frontend tool marked `requiresWriteConsent` puts to the\n * person before its first call in a conversation runs: allow (which covers\n * the rest of the conversation) or decline. `AthenaChat` renders it above the\n * composer; a host with its own chat mounts it there, or builds its own on\n * `useClientToolWriteConsent`. Renders nothing while no call is waiting.\n */\nexport const StatewireClientToolConsentCard: FC<StatewireClientToolConsentCardProps> = ({\n className,\n agentName,\n}) => {\n const { request, allow, decline } = useClientToolWriteConsent();\n if (!request) return null;\n const asker = agentName ?? request.agentName ?? 'The agent';\n return (\n <section\n aria-label=\"Permission required\"\n data-testid=\"client-tool-consent-card\"\n className={cn(\n 'aui-client-tool-consent-card w-full rounded-2xl border border-border bg-card px-4 py-3 text-foreground shadow-xs',\n className,\n )}\n >\n <div className=\"flex items-start gap-3\">\n <MousePointerClickIcon className=\"mt-0.5 size-4 shrink-0 text-muted-foreground\" />\n <div className=\"min-w-0 flex-1\">\n <p className=\"font-semibold text-sm\">Allow this?</p>\n <p className=\"break-words pt-1 text-foreground text-sm leading-snug\">\n {clientToolConsentSentence({ agentName: asker, request })}\n </p>\n <p className=\"pt-1 text-muted-foreground text-sm leading-snug\">\n Allowing covers the rest of this conversation.\n </p>\n </div>\n </div>\n <div className=\"flex flex-wrap items-center gap-2 pt-3\">\n <Button size=\"sm\" onClick={allow}>\n <CheckIcon />\n Allow\n </Button>\n <Button size=\"sm\" variant=\"outline\" onClick={decline}>\n <XIcon />\n Decline\n </Button>\n </div>\n </section>\n );\n};\n"],"names":[],"mappings":";;;;;AAuBO,SAAS,0BAA0B;AAAA,EACxC;AAAA,EACA;AACF,GAGW;AACT,QAAM,YAAY,MAAM;AAAA,IACtB,IAAI,IAAI,QAAQ,MAAM,QAAQ,CAAC,SAAU,KAAK,UAAU,CAAC,KAAK,OAAO,IAAI,CAAA,CAAG,CAAC;AAAA,EAAA;AAE/E,MAAI,UAAU,SAAS,KAAK,UAAU,WAAW,QAAQ,MAAM,QAAQ;AACrE,WAAO,GAAG,SAAS,aAAa,UAAU,SAAS,CAAC;AAAA,EACtD;AACA,QAAM,QAAQ,QAAQ,MAAM,IAAI,CAAC,SAAS,KAAK,IAAI;AACnD,SAAO,GAAG,SAAS,iBAAiB,UAAU,KAAK,CAAC,WAAW,MAAM,WAAW,IAAI,YAAY,QAAQ;AAC1G;AAEA,SAAS,UAAU,OAAkC;AACnD,MAAI,MAAM,UAAU,EAAG,QAAO,MAAM,CAAC,KAAK;AAC1C,SAAO,GAAG,MAAM,MAAM,GAAG,EAAE,EAAE,KAAK,IAAI,CAAC,QAAQ,MAAM,MAAM,SAAS,CAAC,CAAC;AACxE;AASO,MAAM,iCAA0E,CAAC;AAAA,EACtF;AAAA,EACA;AACF,MAAM;AACJ,QAAM,EAAE,SAAS,OAAO,QAAA,IAAY,0BAAA;AACpC,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,QAAQ,aAAa,QAAQ,aAAa;AAChD,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,cAAW;AAAA,MACX,eAAY;AAAA,MACZ,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MAAA;AAAA,MAGF,UAAA;AAAA,QAAA,qBAAC,OAAA,EAAI,WAAU,0BACb,UAAA;AAAA,UAAA,oBAAC,uBAAA,EAAsB,WAAU,+CAAA,CAA+C;AAAA,UAChF,qBAAC,OAAA,EAAI,WAAU,kBACb,UAAA;AAAA,YAAA,oBAAC,KAAA,EAAE,WAAU,yBAAwB,UAAA,eAAW;AAAA,YAChD,oBAAC,KAAA,EAAE,WAAU,yDACV,UAAA,0BAA0B,EAAE,WAAW,OAAO,QAAA,CAAS,EAAA,CAC1D;AAAA,YACA,oBAAC,KAAA,EAAE,WAAU,mDAAkD,UAAA,iDAAA,CAE/D;AAAA,UAAA,EAAA,CACF;AAAA,QAAA,GACF;AAAA,QACA,qBAAC,OAAA,EAAI,WAAU,0CACb,UAAA;AAAA,UAAA,qBAAC,QAAA,EAAO,MAAK,MAAK,SAAS,OACzB,UAAA;AAAA,YAAA,oBAAC,WAAA,EAAU;AAAA,YAAE;AAAA,UAAA,GAEf;AAAA,+BACC,QAAA,EAAO,MAAK,MAAK,SAAQ,WAAU,SAAS,SAC3C,UAAA;AAAA,YAAA,oBAAC,OAAA,EAAM;AAAA,YAAE;AAAA,UAAA,EAAA,CAEX;AAAA,QAAA,EAAA,CACF;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGN;"}
|
|
@@ -73,7 +73,8 @@ function useComposerChatVoice({ options }) {
|
|
|
73
73
|
function ChatVoiceButton({
|
|
74
74
|
voice,
|
|
75
75
|
disabled,
|
|
76
|
-
disabledReason = null
|
|
76
|
+
disabledReason = null,
|
|
77
|
+
appearance = "icon"
|
|
77
78
|
}) {
|
|
78
79
|
const live = voice.phase === "live";
|
|
79
80
|
const connecting = voice.phase === "connecting";
|
|
@@ -89,8 +90,10 @@ function ChatVoiceButton({
|
|
|
89
90
|
"aria-pressed": live && !voice.micMuted,
|
|
90
91
|
"aria-disabled": unavailable || void 0,
|
|
91
92
|
"data-unavailable": unavailable || void 0,
|
|
93
|
+
...appearance === "send" ? { variant: "default" } : {},
|
|
92
94
|
className: cnfast.cn(
|
|
93
95
|
"aui-composer-voice size-8 rounded-full p-0 disabled:opacity-30",
|
|
96
|
+
appearance === "send" && "aui-composer-voice-send",
|
|
94
97
|
unavailable && "cursor-not-allowed opacity-30 hover:bg-transparent"
|
|
95
98
|
),
|
|
96
99
|
disabled: !unavailable && !live && !connecting && disabled,
|
|
@@ -101,7 +104,7 @@ function ChatVoiceButton({
|
|
|
101
104
|
else if (moveHere) void voice.transferHere();
|
|
102
105
|
else void voice.start();
|
|
103
106
|
},
|
|
104
|
-
children: connecting ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2Icon, { className: "size-4 animate-spin" }) : live ? voice.micMuted ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MicOffIcon, { className: "size-4" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MicIcon, { className: "size-4" }) : moveHere ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.PhoneIcon, { className: "size-4" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.HeadphonesIcon, { className: "size-4" })
|
|
107
|
+
children: connecting ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2Icon, { className: "size-4 animate-spin" }) : live ? voice.micMuted ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MicOffIcon, { className: "size-4" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MicIcon, { className: "size-4" }) : moveHere ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.PhoneIcon, { className: "size-4" }) : appearance === "send" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AudioLinesIcon, { className: "size-4" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.HeadphonesIcon, { className: "size-4" })
|
|
105
108
|
}
|
|
106
109
|
);
|
|
107
110
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat-voice-controls.cjs","sources":["../../src/chat/chat-voice-controls.tsx"],"sourcesContent":["import { useAui } from '@assistant-ui/react';\nimport {\n describeVoiceLiveElsewhere,\n playVoiceMoveEarcon,\n useVoiceElapsed,\n useVoiceMovedNotice,\n VOICE_CONTINUE_BY_VOICE_LABEL,\n VOICE_LIVE_TYPED_HINT,\n VOICE_MOVE_CALL_HERE_LABEL,\n VOICE_MOVE_HERE_ACTION,\n} from '@athenaintel/agent-runtime';\nimport { HeadphonesIcon, Loader2Icon, MicIcon, MicOffIcon, PhoneIcon, XIcon } from 'lucide-react';\nimport { useEffect, useMemo, useRef } from 'react';\nimport { athenaDiagnostics } from '../diagnostics/athena-diagnostics';\nimport { createDeepAgentVoiceLiveKitAdapter } from '../voice/livekit';\nimport {\n useAthenaChatVoice,\n type UseAthenaChatVoiceOptions,\n type UseAthenaChatVoiceReturn,\n} from '../voice/chat';\nimport { useChatVoiceEnabled } from '../voice/use-chat-voice-enabled';\nimport { cn } from '../lib/utils';\nimport { Button } from '../ui/button';\nimport { TooltipIconButton } from './tooltip-icon-button';\nimport { useMentionAuth } from '../chat-ui/mentions/suggestions/data/api-client';\nimport { credentialFingerprint, useCatalogPrincipal } from '../catalog/runtime-catalog';\n\n/**\n * `onCallMoved` replaces the move earcon a call plays when it moves to or\n * from this device (pass a no-op to keep it silent); by default the shared\n * one plays.\n */\nexport interface AthenaChatVoiceOptions\n extends Pick<UseAthenaChatVoiceOptions, 'model' | 'livekit' | 'onDiagnostic' | 'onCallMoved'> {}\n\n/** One controller for this composer's controls, text sends and audio sink. */\nexport function useComposerChatVoice({ options }: { options: boolean | AthenaChatVoiceOptions }) {\n const aui = useAui();\n const auth = useMentionAuth();\n const principal = useCatalogPrincipal();\n const enabled = useChatVoiceEnabled({ enabled: options !== false });\n const audioRef = useRef<HTMLAudioElement | null>(null);\n const media = useMemo(\n () => createDeepAgentVoiceLiveKitAdapter({ audioElement: () => audioRef.current }),\n []\n );\n const config = typeof options === 'object' ? options : undefined;\n const voice = useAthenaChatVoice({\n transport: 'livekit',\n livekit: config?.livekit ?? media,\n model: config?.model,\n onDiagnostic:\n config?.onDiagnostic ??\n ((entry) => athenaDiagnostics.emit('voice.chat', { data: { ...entry } })),\n onCallMoved: config?.onCallMoved ?? playVoiceMoveEarcon,\n });\n const active = voice.phase === 'connecting' || voice.phase === 'live';\n const { end, syncProfile } = voice;\n const identity = JSON.stringify([\n auth.backendUrl,\n principal,\n auth.apiKey ? credentialFingerprint({ credential: auth.apiKey }) : null,\n ]);\n const previousScope = useRef({ identity, workspaceId: auth.workspaceId });\n useEffect(() => {\n const previous = previousScope.current;\n const changed =\n previous.identity !== identity ||\n Boolean(\n previous.workspaceId && auth.workspaceId && previous.workspaceId !== auth.workspaceId\n );\n previousScope.current = {\n identity,\n workspaceId:\n previous.identity === identity\n ? auth.workspaceId ?? previous.workspaceId\n : auth.workspaceId,\n };\n if (active && (changed || !enabled)) end();\n }, [identity, auth.workspaceId, active, enabled, end]);\n\n // Model/agent/toolkit/mode picks live in the ambient model context. Read it\n // through the controller at send time, with the same debounce as other hosts.\n const live = voice.phase === 'live';\n useEffect(() => {\n if (!live) return;\n let timer: ReturnType<typeof setTimeout>;\n const schedule = () => {\n clearTimeout(timer);\n timer = setTimeout(syncProfile, 800);\n };\n schedule();\n const unsubscribe = aui.modelContext.subscribe?.(schedule);\n return () => {\n clearTimeout(timer);\n unsubscribe?.();\n };\n }, [aui, live, syncProfile]);\n return { voice, enabled, active, audioRef };\n}\n\nexport function ChatVoiceButton({\n voice,\n disabled,\n disabledReason = null,\n}: {\n voice: UseAthenaChatVoiceReturn;\n disabled: boolean;\n /**\n * Why a voice session can't start now (Athena can't be reached): shown as the tooltip of a\n * control that stays focusable and starts nothing. A session already connecting or live keeps its\n * Cancel and Mute.\n */\n disabledReason?: string | null;\n}) {\n const live = voice.phase === 'live';\n const connecting = voice.phase === 'connecting';\n const unavailable = Boolean(disabledReason) && !live && !connecting;\n // The conversation's call is live on another of the person's devices.\n const moveHere = !live && !connecting && voice.liveElsewhere !== null;\n const label = connecting\n ? 'Cancel voice connection'\n : live\n ? voice.micMuted\n ? 'Unmute microphone'\n : 'Mute microphone'\n : moveHere\n ? VOICE_MOVE_CALL_HERE_LABEL\n : 'Start voice session';\n return (\n <TooltipIconButton\n type=\"button\"\n tooltip={unavailable && disabledReason ? disabledReason : label}\n aria-label={label}\n aria-pressed={live && !voice.micMuted}\n aria-disabled={unavailable || undefined}\n data-unavailable={unavailable || undefined}\n className={cn(\n 'aui-composer-voice size-8 rounded-full p-0 disabled:opacity-30',\n unavailable && 'cursor-not-allowed opacity-30 hover:bg-transparent',\n )}\n disabled={!unavailable && !live && !connecting && disabled}\n onClick={() => {\n if (unavailable) return;\n if (connecting) voice.end();\n else if (live) voice.setMicMuted(!voice.micMuted);\n else if (moveHere) void voice.transferHere();\n else void voice.start();\n }}\n >\n {connecting ? (\n <Loader2Icon className=\"size-4 animate-spin\" />\n ) : live ? (\n voice.micMuted ? (\n <MicOffIcon className=\"size-4\" />\n ) : (\n <MicIcon className=\"size-4\" />\n )\n ) : moveHere ? (\n <PhoneIcon className=\"size-4\" />\n ) : (\n <HeadphonesIcon className=\"size-4\" />\n )}\n </TooltipIconButton>\n );\n}\n\n/**\n * A host's \"Continue by voice\" offer for the open conversation: a handover the\n * person sent in voice mode landed here. `onDone` runs once they answer it —\n * took it, dismissed it, or typed instead — so the host clears the offer.\n */\nexport interface AthenaChatContinueByVoice {\n onDone: () => void;\n}\n\n/**\n * The one-tap \"Continue by voice\" above the composer: it starts a call whose\n * model opens by saying where things stand. Shown while voice is idle here and\n * no call is live on another of the person's devices (\"Move here\" covers that).\n * Nothing starts the microphone without the tap.\n */\nexport function ChatVoiceContinueOffer({\n voice,\n offer,\n disabled = false,\n disabledReason = null,\n}: {\n voice: UseAthenaChatVoiceReturn;\n offer: AthenaChatContinueByVoice | null;\n /** Starting would capture the microphone now (dictation, an upload, no channel). */\n disabled?: boolean;\n /** Why voice can't start now (Athena can't be reached), as the action's tooltip. */\n disabledReason?: string | null;\n}) {\n if (!offer || voice.phase !== 'idle' || voice.liveElsewhere !== null) return null;\n return (\n <div className=\"aui-chat-voice-continue flex items-center gap-1 px-2 pb-2\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"xs\"\n title={disabledReason ?? undefined}\n disabled={disabled || Boolean(disabledReason)}\n onClick={() => {\n offer.onDone();\n void voice.start({ opening: 'recap' });\n }}\n >\n <MicIcon aria-hidden=\"true\" />\n {VOICE_CONTINUE_BY_VOICE_LABEL}\n </Button>\n <TooltipIconButton\n type=\"button\"\n tooltip=\"Not now\"\n aria-label=\"Not now\"\n className=\"size-6 rounded-md p-0\"\n onClick={offer.onDone}\n >\n <XIcon className=\"size-3\" />\n </TooltipIconButton>\n </div>\n );\n}\n\nexport function ChatVoiceStatus({\n voice,\n offerMove = false,\n moveDisabled = false,\n moveDisabledReason = null,\n}: {\n voice: UseAthenaChatVoiceReturn;\n /** Whether this composer may take a call from another device (its voice is enabled). */\n offerMove?: boolean;\n /** Moving the call here would capture the microphone now (dictation, an upload, no channel). */\n moveDisabled?: boolean;\n /** Why the call can't move here now (Athena can't be reached), as the action's tooltip. */\n moveDisabledReason?: string | null;\n}) {\n const elapsed = useVoiceElapsed(voice.liveSince);\n const movedNotice = useVoiceMovedNotice(voice.movedTo);\n const active = voice.phase === 'connecting' || voice.phase === 'live';\n // \"Live call on your phone · Move here\", or briefly after this composer's call moved away,\n // \"Call moved to your phone\".\n const elsewhere = offerMove && !active && movedNotice === null ? voice.liveElsewhere : null;\n if (!active && !voice.error && !voice.handoffTask && movedNotice === null && elsewhere === null)\n return null;\n const moveUnavailable = moveDisabled || Boolean(moveDisabledReason);\n return (\n <div className=\"aui-chat-voice-status flex flex-col gap-1 px-2 pb-2 text-xs text-muted-foreground\">\n {/* No end control of its own: the composer's Stop ends the session\n (and any run), so there is one way to stop everything. */}\n {active && (\n <span role=\"status\">\n {voice.phase === 'connecting'\n ? 'Connecting voice…'\n : `Voice ${voice.micMuted ? 'muted' : 'live'}${elapsed ? ` · ${elapsed}` : ''}`}\n </span>\n )}\n {voice.phase === 'live' && <span>{VOICE_LIVE_TYPED_HINT}</span>}\n {voice.activeTask && <span>Athena is working: {voice.activeTask.text}</span>}\n {voice.handoffTask && (\n <span>Voice ended. Athena is still working: {voice.handoffTask.text}</span>\n )}\n {voice.notice && <span role=\"status\">{voice.notice}</span>}\n {voice.error && (\n <span role=\"alert\" className=\"text-destructive\">\n {voice.error}\n </span>\n )}\n {movedNotice !== null && <span role=\"status\">{movedNotice}</span>}\n {elsewhere !== null && (\n <span className=\"aui-chat-voice-elsewhere flex items-center gap-1\">\n <span className=\"min-w-0 truncate\">{describeVoiceLiveElsewhere(elsewhere.surface)}</span>\n <span aria-hidden=\"true\">·</span>\n <button\n type=\"button\"\n title={moveDisabledReason ?? undefined}\n disabled={moveUnavailable}\n onClick={() => void voice.transferHere()}\n className=\"shrink-0 font-medium text-foreground underline-offset-2 hover:underline disabled:cursor-not-allowed disabled:opacity-50\"\n >\n {VOICE_MOVE_HERE_ACTION}\n </button>\n </span>\n )}\n </div>\n );\n}\n"],"names":["useAui","useMentionAuth","useCatalogPrincipal","useChatVoiceEnabled","useRef","useMemo","createDeepAgentVoiceLiveKitAdapter","useAthenaChatVoice","athenaDiagnostics","playVoiceMoveEarcon","credentialFingerprint","useEffect","VOICE_MOVE_CALL_HERE_LABEL","jsx","TooltipIconButton","cn","Loader2Icon","MicOffIcon","MicIcon","PhoneIcon","HeadphonesIcon","jsxs","Button","VOICE_CONTINUE_BY_VOICE_LABEL","XIcon","useVoiceElapsed","useVoiceMovedNotice","VOICE_LIVE_TYPED_HINT","describeVoiceLiveElsewhere","VOICE_MOVE_HERE_ACTION"],"mappings":";;;;;;;;;;;;;;;;;AAoCO,SAAS,qBAAqB,EAAE,WAA0D;AAC/F,QAAM,MAAMA,MAAAA,OAAA;AACZ,QAAM,OAAOC,UAAAA,eAAA;AACb,QAAM,YAAYC,eAAAA,oBAAA;AAClB,QAAM,UAAUC,oBAAAA,oBAAoB,EAAE,SAAS,YAAY,OAAO;AAClE,QAAM,WAAWC,WAAAA,OAAgC,IAAI;AACrD,QAAM,QAAQC,WAAAA;AAAAA,IACZ,MAAMC,cAAAA,mCAAmC,EAAE,cAAc,MAAM,SAAS,SAAS;AAAA,IACjF,CAAA;AAAA,EAAC;AAEH,QAAM,SAAS,OAAO,YAAY,WAAW,UAAU;AACvD,QAAM,QAAQC,kBAAAA,kBAAmB;AAAA,IAC/B,WAAW;AAAA,IACX,UAAS,iCAAQ,YAAW;AAAA,IAC5B,OAAO,iCAAQ;AAAA,IACf,eACE,iCAAQ,kBACP,CAAC,UAAUC,kBAAAA,kBAAkB,KAAK,cAAc,EAAE,MAAM,EAAE,GAAG,MAAA,GAAS;AAAA,IACzE,cAAa,iCAAQ,gBAAeC,YAAAA;AAAAA,EAAA,CACrC;AACD,QAAM,SAAS,MAAM,UAAU,gBAAgB,MAAM,UAAU;AAC/D,QAAM,EAAE,KAAK,YAAA,IAAgB;AAC7B,QAAM,WAAW,KAAK,UAAU;AAAA,IAC9B,KAAK;AAAA,IACL;AAAA,IACA,KAAK,SAASC,qCAAsB,EAAE,YAAY,KAAK,OAAA,CAAQ,IAAI;AAAA,EAAA,CACpE;AACD,QAAM,gBAAgBN,WAAAA,OAAO,EAAE,UAAU,aAAa,KAAK,aAAa;AACxEO,aAAAA,UAAU,MAAM;AACd,UAAM,WAAW,cAAc;AAC/B,UAAM,UACJ,SAAS,aAAa,YACtB;AAAA,MACE,SAAS,eAAe,KAAK,eAAe,SAAS,gBAAgB,KAAK;AAAA,IAAA;AAE9E,kBAAc,UAAU;AAAA,MACtB;AAAA,MACA,aACE,SAAS,aAAa,WAClB,KAAK,eAAe,SAAS,cAC7B,KAAK;AAAA,IAAA;AAEb,QAAI,WAAW,WAAW,CAAC,SAAU,KAAA;AAAA,EACvC,GAAG,CAAC,UAAU,KAAK,aAAa,QAAQ,SAAS,GAAG,CAAC;AAIrD,QAAM,OAAO,MAAM,UAAU;AAC7BA,aAAAA,UAAU,MAAM;;AACd,QAAI,CAAC,KAAM;AACX,QAAI;AACJ,UAAM,WAAW,MAAM;AACrB,mBAAa,KAAK;AAClB,cAAQ,WAAW,aAAa,GAAG;AAAA,IACrC;AACA,aAAA;AACA,UAAM,eAAc,eAAI,cAAa,cAAjB,4BAA6B;AACjD,WAAO,MAAM;AACX,mBAAa,KAAK;AAClB;AAAA,IACF;AAAA,EACF,GAAG,CAAC,KAAK,MAAM,WAAW,CAAC;AAC3B,SAAO,EAAE,OAAO,SAAS,QAAQ,SAAA;AACnC;AAEO,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA,iBAAiB;AACnB,GASG;AACD,QAAM,OAAO,MAAM,UAAU;AAC7B,QAAM,aAAa,MAAM,UAAU;AACnC,QAAM,cAAc,QAAQ,cAAc,KAAK,CAAC,QAAQ,CAAC;AAEzD,QAAM,WAAW,CAAC,QAAQ,CAAC,cAAc,MAAM,kBAAkB;AACjE,QAAM,QAAQ,aACV,4BACA,OACA,MAAM,WACJ,sBACA,oBACF,WACAC,kBAAAA,6BACA;AACJ,SACEC,2BAAAA;AAAAA,IAACC,kBAAAA;AAAAA,IAAA;AAAA,MACC,MAAK;AAAA,MACL,SAAS,eAAe,iBAAiB,iBAAiB;AAAA,MAC1D,cAAY;AAAA,MACZ,gBAAc,QAAQ,CAAC,MAAM;AAAA,MAC7B,iBAAe,eAAe;AAAA,MAC9B,oBAAkB,eAAe;AAAA,MACjC,WAAWC,OAAAA;AAAAA,QACT;AAAA,QACA,eAAe;AAAA,MAAA;AAAA,MAEjB,UAAU,CAAC,eAAe,CAAC,QAAQ,CAAC,cAAc;AAAA,MAClD,SAAS,MAAM;AACb,YAAI,YAAa;AACjB,YAAI,kBAAkB,IAAA;AAAA,iBACb,KAAM,OAAM,YAAY,CAAC,MAAM,QAAQ;AAAA,iBACvC,SAAU,MAAK,MAAM,aAAA;AAAA,YACzB,MAAK,MAAM,MAAA;AAAA,MAClB;AAAA,MAEC,UAAA,aACCF,2BAAAA,IAACG,YAAAA,aAAA,EAAY,WAAU,uBAAsB,IAC3C,OACF,MAAM,WACJH,2BAAAA,IAACI,YAAAA,YAAA,EAAW,WAAU,UAAS,IAE/BJ,2BAAAA,IAACK,YAAAA,SAAA,EAAQ,WAAU,SAAA,CAAS,IAE5B,WACFL,2BAAAA,IAACM,YAAAA,WAAA,EAAU,WAAU,SAAA,CAAS,IAE9BN,2BAAAA,IAACO,YAAAA,gBAAA,EAAe,WAAU,SAAA,CAAS;AAAA,IAAA;AAAA,EAAA;AAI3C;AAiBO,SAAS,uBAAuB;AAAA,EACrC;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,iBAAiB;AACnB,GAOG;AACD,MAAI,CAAC,SAAS,MAAM,UAAU,UAAU,MAAM,kBAAkB,KAAM,QAAO;AAC7E,SACEC,2BAAAA,KAAC,OAAA,EAAI,WAAU,6DACb,UAAA;AAAA,IAAAA,2BAAAA;AAAAA,MAACC,OAAAA;AAAAA,MAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAQ;AAAA,QACR,MAAK;AAAA,QACL,OAAO,kBAAkB;AAAA,QACzB,UAAU,YAAY,QAAQ,cAAc;AAAA,QAC5C,SAAS,MAAM;AACb,gBAAM,OAAA;AACN,eAAK,MAAM,MAAM,EAAE,SAAS,SAAS;AAAA,QACvC;AAAA,QAEA,UAAA;AAAA,UAAAT,2BAAAA,IAACK,YAAAA,SAAA,EAAQ,eAAY,OAAA,CAAO;AAAA,UAC3BK,kBAAAA;AAAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,IAEHV,2BAAAA;AAAAA,MAACC,kBAAAA;AAAAA,MAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAQ;AAAA,QACR,cAAW;AAAA,QACX,WAAU;AAAA,QACV,SAAS,MAAM;AAAA,QAEf,UAAAD,2BAAAA,IAACW,YAAAA,OAAA,EAAM,WAAU,SAAA,CAAS;AAAA,MAAA;AAAA,IAAA;AAAA,EAC5B,GACF;AAEJ;AAEO,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA,YAAY;AAAA,EACZ,eAAe;AAAA,EACf,qBAAqB;AACvB,GAQG;AACD,QAAM,UAAUC,kBAAAA,gBAAgB,MAAM,SAAS;AAC/C,QAAM,cAAcC,kBAAAA,oBAAoB,MAAM,OAAO;AACrD,QAAM,SAAS,MAAM,UAAU,gBAAgB,MAAM,UAAU;AAG/D,QAAM,YAAY,aAAa,CAAC,UAAU,gBAAgB,OAAO,MAAM,gBAAgB;AACvF,MAAI,CAAC,UAAU,CAAC,MAAM,SAAS,CAAC,MAAM,eAAe,gBAAgB,QAAQ,cAAc;AACzF,WAAO;AACT,QAAM,kBAAkB,gBAAgB,QAAQ,kBAAkB;AAClE,SACEL,2BAAAA,KAAC,OAAA,EAAI,WAAU,qFAGZ,UAAA;AAAA,IAAA,yCACE,QAAA,EAAK,MAAK,UACR,UAAA,MAAM,UAAU,eACb,sBACA,SAAS,MAAM,WAAW,UAAU,MAAM,GAAG,UAAU,MAAM,OAAO,KAAK,EAAE,GAAA,CACjF;AAAA,IAED,MAAM,UAAU,UAAUR,2BAAAA,IAAC,UAAM,UAAAc,kBAAAA,uBAAsB;AAAA,IACvD,MAAM,cAAcN,2BAAAA,KAAC,QAAA,EAAK,UAAA;AAAA,MAAA;AAAA,MAAoB,MAAM,WAAW;AAAA,IAAA,GAAK;AAAA,IACpE,MAAM,eACLA,2BAAAA,KAAC,QAAA,EAAK,UAAA;AAAA,MAAA;AAAA,MAAuC,MAAM,YAAY;AAAA,IAAA,GAAK;AAAA,IAErE,MAAM,UAAUR,2BAAAA,IAAC,UAAK,MAAK,UAAU,gBAAM,QAAO;AAAA,IAClD,MAAM,SACLA,+BAAC,QAAA,EAAK,MAAK,SAAQ,WAAU,oBAC1B,UAAA,MAAM,MAAA,CACT;AAAA,IAED,gBAAgB,QAAQA,2BAAAA,IAAC,QAAA,EAAK,MAAK,UAAU,UAAA,aAAY;AAAA,IACzD,cAAc,QACbQ,gCAAC,QAAA,EAAK,WAAU,oDACd,UAAA;AAAA,MAAAR,+BAAC,UAAK,WAAU,oBAAoB,UAAAe,6CAA2B,UAAU,OAAO,GAAE;AAAA,MAClFf,2BAAAA,IAAC,QAAA,EAAK,eAAY,QAAO,UAAA,KAAC;AAAA,MAC1BA,2BAAAA;AAAAA,QAAC;AAAA,QAAA;AAAA,UACC,MAAK;AAAA,UACL,OAAO,sBAAsB;AAAA,UAC7B,UAAU;AAAA,UACV,SAAS,MAAM,KAAK,MAAM,aAAA;AAAA,UAC1B,WAAU;AAAA,UAET,UAAAgB,kBAAAA;AAAAA,QAAA;AAAA,MAAA;AAAA,IACH,EAAA,CACF;AAAA,EAAA,GAEJ;AAEJ;;;;;"}
|
|
1
|
+
{"version":3,"file":"chat-voice-controls.cjs","sources":["../../src/chat/chat-voice-controls.tsx"],"sourcesContent":["import { useAui } from '@assistant-ui/react';\nimport {\n describeVoiceLiveElsewhere,\n playVoiceMoveEarcon,\n useVoiceElapsed,\n useVoiceMovedNotice,\n VOICE_CONTINUE_BY_VOICE_LABEL,\n VOICE_LIVE_TYPED_HINT,\n VOICE_MOVE_CALL_HERE_LABEL,\n VOICE_MOVE_HERE_ACTION,\n} from '@athenaintel/agent-runtime';\nimport {\n AudioLinesIcon,\n HeadphonesIcon,\n Loader2Icon,\n MicIcon,\n MicOffIcon,\n PhoneIcon,\n XIcon,\n} from 'lucide-react';\nimport { useEffect, useMemo, useRef } from 'react';\nimport { athenaDiagnostics } from '../diagnostics/athena-diagnostics';\nimport { createDeepAgentVoiceLiveKitAdapter } from '../voice/livekit';\nimport {\n useAthenaChatVoice,\n type UseAthenaChatVoiceOptions,\n type UseAthenaChatVoiceReturn,\n} from '../voice/chat';\nimport { useChatVoiceEnabled } from '../voice/use-chat-voice-enabled';\nimport { cn } from '../lib/utils';\nimport { Button } from '../ui/button';\nimport { TooltipIconButton } from './tooltip-icon-button';\nimport { useMentionAuth } from '../chat-ui/mentions/suggestions/data/api-client';\nimport { credentialFingerprint, useCatalogPrincipal } from '../catalog/runtime-catalog';\n\n/**\n * `onCallMoved` replaces the move earcon a call plays when it moves to or\n * from this device (pass a no-op to keep it silent); by default the shared\n * one plays.\n */\nexport interface AthenaChatVoiceOptions\n extends Pick<UseAthenaChatVoiceOptions, 'model' | 'livekit' | 'onDiagnostic' | 'onCallMoved'> {}\n\n/** One controller for this composer's controls, text sends and audio sink. */\nexport function useComposerChatVoice({ options }: { options: boolean | AthenaChatVoiceOptions }) {\n const aui = useAui();\n const auth = useMentionAuth();\n const principal = useCatalogPrincipal();\n const enabled = useChatVoiceEnabled({ enabled: options !== false });\n const audioRef = useRef<HTMLAudioElement | null>(null);\n const media = useMemo(\n () => createDeepAgentVoiceLiveKitAdapter({ audioElement: () => audioRef.current }),\n []\n );\n const config = typeof options === 'object' ? options : undefined;\n const voice = useAthenaChatVoice({\n transport: 'livekit',\n livekit: config?.livekit ?? media,\n model: config?.model,\n onDiagnostic:\n config?.onDiagnostic ??\n ((entry) => athenaDiagnostics.emit('voice.chat', { data: { ...entry } })),\n onCallMoved: config?.onCallMoved ?? playVoiceMoveEarcon,\n });\n const active = voice.phase === 'connecting' || voice.phase === 'live';\n const { end, syncProfile } = voice;\n const identity = JSON.stringify([\n auth.backendUrl,\n principal,\n auth.apiKey ? credentialFingerprint({ credential: auth.apiKey }) : null,\n ]);\n const previousScope = useRef({ identity, workspaceId: auth.workspaceId });\n useEffect(() => {\n const previous = previousScope.current;\n const changed =\n previous.identity !== identity ||\n Boolean(\n previous.workspaceId && auth.workspaceId && previous.workspaceId !== auth.workspaceId\n );\n previousScope.current = {\n identity,\n workspaceId:\n previous.identity === identity\n ? auth.workspaceId ?? previous.workspaceId\n : auth.workspaceId,\n };\n if (active && (changed || !enabled)) end();\n }, [identity, auth.workspaceId, active, enabled, end]);\n\n // Model/agent/toolkit/mode picks live in the ambient model context. Read it\n // through the controller at send time, with the same debounce as other hosts.\n const live = voice.phase === 'live';\n useEffect(() => {\n if (!live) return;\n let timer: ReturnType<typeof setTimeout>;\n const schedule = () => {\n clearTimeout(timer);\n timer = setTimeout(syncProfile, 800);\n };\n schedule();\n const unsubscribe = aui.modelContext.subscribe?.(schedule);\n return () => {\n clearTimeout(timer);\n unsubscribe?.();\n };\n }, [aui, live, syncProfile]);\n return { voice, enabled, active, audioRef };\n}\n\nexport function ChatVoiceButton({\n voice,\n disabled,\n disabledReason = null,\n appearance = 'icon',\n}: {\n voice: UseAthenaChatVoiceReturn;\n disabled: boolean;\n /**\n * `send`: drawn as the composer's Send (an ink circle with a waveform), where it stands in for Send\n * while the draft is empty (`AthenaChatComposerSlots.voiceInSend`). `icon` (default): a plain icon.\n */\n appearance?: 'icon' | 'send';\n /**\n * Why a voice session can't start now (Athena can't be reached): shown as the tooltip of a\n * control that stays focusable and starts nothing. A session already connecting or live keeps its\n * Cancel and Mute.\n */\n disabledReason?: string | null;\n}) {\n const live = voice.phase === 'live';\n const connecting = voice.phase === 'connecting';\n const unavailable = Boolean(disabledReason) && !live && !connecting;\n // The conversation's call is live on another of the person's devices.\n const moveHere = !live && !connecting && voice.liveElsewhere !== null;\n const label = connecting\n ? 'Cancel voice connection'\n : live\n ? voice.micMuted\n ? 'Unmute microphone'\n : 'Mute microphone'\n : moveHere\n ? VOICE_MOVE_CALL_HERE_LABEL\n : 'Start voice session';\n return (\n <TooltipIconButton\n type=\"button\"\n tooltip={unavailable && disabledReason ? disabledReason : label}\n aria-label={label}\n aria-pressed={live && !voice.micMuted}\n aria-disabled={unavailable || undefined}\n data-unavailable={unavailable || undefined}\n {...(appearance === 'send' ? { variant: 'default' as const } : {})}\n className={cn(\n 'aui-composer-voice size-8 rounded-full p-0 disabled:opacity-30',\n appearance === 'send' && 'aui-composer-voice-send',\n unavailable && 'cursor-not-allowed opacity-30 hover:bg-transparent',\n )}\n disabled={!unavailable && !live && !connecting && disabled}\n onClick={() => {\n if (unavailable) return;\n if (connecting) voice.end();\n else if (live) voice.setMicMuted(!voice.micMuted);\n else if (moveHere) void voice.transferHere();\n else void voice.start();\n }}\n >\n {connecting ? (\n <Loader2Icon className=\"size-4 animate-spin\" />\n ) : live ? (\n voice.micMuted ? (\n <MicOffIcon className=\"size-4\" />\n ) : (\n <MicIcon className=\"size-4\" />\n )\n ) : moveHere ? (\n <PhoneIcon className=\"size-4\" />\n ) : appearance === 'send' ? (\n <AudioLinesIcon className=\"size-4\" />\n ) : (\n <HeadphonesIcon className=\"size-4\" />\n )}\n </TooltipIconButton>\n );\n}\n\n/**\n * A host's \"Continue by voice\" offer for the open conversation: a handover the\n * person sent in voice mode landed here. `onDone` runs once they answer it —\n * took it, dismissed it, or typed instead — so the host clears the offer.\n */\nexport interface AthenaChatContinueByVoice {\n onDone: () => void;\n}\n\n/**\n * The one-tap \"Continue by voice\" above the composer: it starts a call whose\n * model opens by saying where things stand. Shown while voice is idle here and\n * no call is live on another of the person's devices (\"Move here\" covers that).\n * Nothing starts the microphone without the tap.\n */\nexport function ChatVoiceContinueOffer({\n voice,\n offer,\n disabled = false,\n disabledReason = null,\n}: {\n voice: UseAthenaChatVoiceReturn;\n offer: AthenaChatContinueByVoice | null;\n /** Starting would capture the microphone now (dictation, an upload, no channel). */\n disabled?: boolean;\n /** Why voice can't start now (Athena can't be reached), as the action's tooltip. */\n disabledReason?: string | null;\n}) {\n if (!offer || voice.phase !== 'idle' || voice.liveElsewhere !== null) return null;\n return (\n <div className=\"aui-chat-voice-continue flex items-center gap-1 px-2 pb-2\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"xs\"\n title={disabledReason ?? undefined}\n disabled={disabled || Boolean(disabledReason)}\n onClick={() => {\n offer.onDone();\n void voice.start({ opening: 'recap' });\n }}\n >\n <MicIcon aria-hidden=\"true\" />\n {VOICE_CONTINUE_BY_VOICE_LABEL}\n </Button>\n <TooltipIconButton\n type=\"button\"\n tooltip=\"Not now\"\n aria-label=\"Not now\"\n className=\"size-6 rounded-md p-0\"\n onClick={offer.onDone}\n >\n <XIcon className=\"size-3\" />\n </TooltipIconButton>\n </div>\n );\n}\n\nexport function ChatVoiceStatus({\n voice,\n offerMove = false,\n moveDisabled = false,\n moveDisabledReason = null,\n}: {\n voice: UseAthenaChatVoiceReturn;\n /** Whether this composer may take a call from another device (its voice is enabled). */\n offerMove?: boolean;\n /** Moving the call here would capture the microphone now (dictation, an upload, no channel). */\n moveDisabled?: boolean;\n /** Why the call can't move here now (Athena can't be reached), as the action's tooltip. */\n moveDisabledReason?: string | null;\n}) {\n const elapsed = useVoiceElapsed(voice.liveSince);\n const movedNotice = useVoiceMovedNotice(voice.movedTo);\n const active = voice.phase === 'connecting' || voice.phase === 'live';\n // \"Live call on your phone · Move here\", or briefly after this composer's call moved away,\n // \"Call moved to your phone\".\n const elsewhere = offerMove && !active && movedNotice === null ? voice.liveElsewhere : null;\n if (!active && !voice.error && !voice.handoffTask && movedNotice === null && elsewhere === null)\n return null;\n const moveUnavailable = moveDisabled || Boolean(moveDisabledReason);\n return (\n <div className=\"aui-chat-voice-status flex flex-col gap-1 px-2 pb-2 text-xs text-muted-foreground\">\n {/* No end control of its own: the composer's Stop ends the session\n (and any run), so there is one way to stop everything. */}\n {active && (\n <span role=\"status\">\n {voice.phase === 'connecting'\n ? 'Connecting voice…'\n : `Voice ${voice.micMuted ? 'muted' : 'live'}${elapsed ? ` · ${elapsed}` : ''}`}\n </span>\n )}\n {voice.phase === 'live' && <span>{VOICE_LIVE_TYPED_HINT}</span>}\n {voice.activeTask && <span>Athena is working: {voice.activeTask.text}</span>}\n {voice.handoffTask && (\n <span>Voice ended. Athena is still working: {voice.handoffTask.text}</span>\n )}\n {voice.notice && <span role=\"status\">{voice.notice}</span>}\n {voice.error && (\n <span role=\"alert\" className=\"text-destructive\">\n {voice.error}\n </span>\n )}\n {movedNotice !== null && <span role=\"status\">{movedNotice}</span>}\n {elsewhere !== null && (\n <span className=\"aui-chat-voice-elsewhere flex items-center gap-1\">\n <span className=\"min-w-0 truncate\">{describeVoiceLiveElsewhere(elsewhere.surface)}</span>\n <span aria-hidden=\"true\">·</span>\n <button\n type=\"button\"\n title={moveDisabledReason ?? undefined}\n disabled={moveUnavailable}\n onClick={() => void voice.transferHere()}\n className=\"shrink-0 font-medium text-foreground underline-offset-2 hover:underline disabled:cursor-not-allowed disabled:opacity-50\"\n >\n {VOICE_MOVE_HERE_ACTION}\n </button>\n </span>\n )}\n </div>\n );\n}\n"],"names":["useAui","useMentionAuth","useCatalogPrincipal","useChatVoiceEnabled","useRef","useMemo","createDeepAgentVoiceLiveKitAdapter","useAthenaChatVoice","athenaDiagnostics","playVoiceMoveEarcon","credentialFingerprint","useEffect","VOICE_MOVE_CALL_HERE_LABEL","jsx","TooltipIconButton","cn","Loader2Icon","MicOffIcon","MicIcon","PhoneIcon","AudioLinesIcon","HeadphonesIcon","jsxs","Button","VOICE_CONTINUE_BY_VOICE_LABEL","XIcon","useVoiceElapsed","useVoiceMovedNotice","VOICE_LIVE_TYPED_HINT","describeVoiceLiveElsewhere","VOICE_MOVE_HERE_ACTION"],"mappings":"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"}
|
|
@@ -15,9 +15,14 @@ export declare function useComposerChatVoice({ options }: {
|
|
|
15
15
|
active: boolean;
|
|
16
16
|
audioRef: import("react").RefObject<HTMLAudioElement | null>;
|
|
17
17
|
};
|
|
18
|
-
export declare function ChatVoiceButton({ voice, disabled, disabledReason, }: {
|
|
18
|
+
export declare function ChatVoiceButton({ voice, disabled, disabledReason, appearance, }: {
|
|
19
19
|
voice: UseAthenaChatVoiceReturn;
|
|
20
20
|
disabled: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* `send`: drawn as the composer's Send (an ink circle with a waveform), where it stands in for Send
|
|
23
|
+
* while the draft is empty (`AthenaChatComposerSlots.voiceInSend`). `icon` (default): a plain icon.
|
|
24
|
+
*/
|
|
25
|
+
appearance?: 'icon' | 'send';
|
|
21
26
|
/**
|
|
22
27
|
* Why a voice session can't start now (Athena can't be reached): shown as the tooltip of a
|
|
23
28
|
* control that stays focusable and starts nothing. A session already connecting or live keeps its
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useAui } from "@assistant-ui/react";
|
|
3
|
-
import { Loader2Icon, MicOffIcon, MicIcon, PhoneIcon, HeadphonesIcon, XIcon } from "lucide-react";
|
|
3
|
+
import { Loader2Icon, MicOffIcon, MicIcon, PhoneIcon, AudioLinesIcon, HeadphonesIcon, XIcon } from "lucide-react";
|
|
4
4
|
import { useRef, useMemo, useEffect } from "react";
|
|
5
5
|
import { athenaDiagnostics } from "../diagnostics/athena-diagnostics.js";
|
|
6
6
|
import { createDeepAgentVoiceLiveKitAdapter } from "../voice/livekit.js";
|
|
@@ -71,7 +71,8 @@ function useComposerChatVoice({ options }) {
|
|
|
71
71
|
function ChatVoiceButton({
|
|
72
72
|
voice,
|
|
73
73
|
disabled,
|
|
74
|
-
disabledReason = null
|
|
74
|
+
disabledReason = null,
|
|
75
|
+
appearance = "icon"
|
|
75
76
|
}) {
|
|
76
77
|
const live = voice.phase === "live";
|
|
77
78
|
const connecting = voice.phase === "connecting";
|
|
@@ -87,8 +88,10 @@ function ChatVoiceButton({
|
|
|
87
88
|
"aria-pressed": live && !voice.micMuted,
|
|
88
89
|
"aria-disabled": unavailable || void 0,
|
|
89
90
|
"data-unavailable": unavailable || void 0,
|
|
91
|
+
...appearance === "send" ? { variant: "default" } : {},
|
|
90
92
|
className: cn(
|
|
91
93
|
"aui-composer-voice size-8 rounded-full p-0 disabled:opacity-30",
|
|
94
|
+
appearance === "send" && "aui-composer-voice-send",
|
|
92
95
|
unavailable && "cursor-not-allowed opacity-30 hover:bg-transparent"
|
|
93
96
|
),
|
|
94
97
|
disabled: !unavailable && !live && !connecting && disabled,
|
|
@@ -99,7 +102,7 @@ function ChatVoiceButton({
|
|
|
99
102
|
else if (moveHere) void voice.transferHere();
|
|
100
103
|
else void voice.start();
|
|
101
104
|
},
|
|
102
|
-
children: connecting ? /* @__PURE__ */ jsx(Loader2Icon, { className: "size-4 animate-spin" }) : live ? voice.micMuted ? /* @__PURE__ */ jsx(MicOffIcon, { className: "size-4" }) : /* @__PURE__ */ jsx(MicIcon, { className: "size-4" }) : moveHere ? /* @__PURE__ */ jsx(PhoneIcon, { className: "size-4" }) : /* @__PURE__ */ jsx(HeadphonesIcon, { className: "size-4" })
|
|
105
|
+
children: connecting ? /* @__PURE__ */ jsx(Loader2Icon, { className: "size-4 animate-spin" }) : live ? voice.micMuted ? /* @__PURE__ */ jsx(MicOffIcon, { className: "size-4" }) : /* @__PURE__ */ jsx(MicIcon, { className: "size-4" }) : moveHere ? /* @__PURE__ */ jsx(PhoneIcon, { className: "size-4" }) : appearance === "send" ? /* @__PURE__ */ jsx(AudioLinesIcon, { className: "size-4" }) : /* @__PURE__ */ jsx(HeadphonesIcon, { className: "size-4" })
|
|
103
106
|
}
|
|
104
107
|
);
|
|
105
108
|
}
|