@athenaintel/react 0.27.0 → 0.28.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 +17 -1
- package/dist/chat/StatewireApprovalCard.cjs +19 -0
- package/dist/chat/StatewireApprovalCard.cjs.map +1 -1
- package/dist/chat/StatewireApprovalCard.js +19 -0
- package/dist/chat/StatewireApprovalCard.js.map +1 -1
- package/dist/chat/message-time.cjs.map +1 -1
- package/dist/chat/message-time.d.ts +4 -3
- package/dist/chat/message-time.js.map +1 -1
- package/dist/composer/QuoteContext.cjs +13 -1
- package/dist/composer/QuoteContext.cjs.map +1 -1
- package/dist/composer/QuoteContext.d.ts +11 -1
- package/dist/composer/QuoteContext.js +13 -1
- package/dist/composer/QuoteContext.js.map +1 -1
- package/dist/diagnostics/athena-diagnostics.cjs.map +1 -1
- package/dist/diagnostics/athena-diagnostics.d.ts +1 -1
- package/dist/diagnostics/athena-diagnostics.js.map +1 -1
- package/dist/lib/athena-urls.cjs +1 -1
- package/dist/lib/athena-urls.cjs.map +1 -1
- package/dist/lib/athena-urls.js +1 -1
- package/dist/lib/athena-urls.js.map +1 -1
- package/dist/lib/posthog/mask-sensitive-input.cjs +34 -0
- package/dist/lib/posthog/mask-sensitive-input.cjs.map +1 -0
- package/dist/lib/posthog/mask-sensitive-input.d.ts +9 -0
- package/dist/lib/posthog/mask-sensitive-input.js +34 -0
- package/dist/lib/posthog/mask-sensitive-input.js.map +1 -0
- package/dist/packages/agent-runtime/src/conversation-index.cjs +2 -1
- package/dist/packages/agent-runtime/src/conversation-index.cjs.map +1 -1
- package/dist/packages/agent-runtime/src/conversation-index.js +2 -1
- package/dist/packages/agent-runtime/src/conversation-index.js.map +1 -1
- package/dist/packages/agent-runtime/src/generated/runconfig-contract.cjs +2 -0
- package/dist/packages/agent-runtime/src/generated/runconfig-contract.cjs.map +1 -1
- package/dist/packages/agent-runtime/src/generated/runconfig-contract.js +2 -0
- package/dist/packages/agent-runtime/src/generated/runconfig-contract.js.map +1 -1
- package/dist/packages/agent-runtime/src/interrupts.cjs +26 -8
- package/dist/packages/agent-runtime/src/interrupts.cjs.map +1 -1
- package/dist/packages/agent-runtime/src/interrupts.js +26 -8
- package/dist/packages/agent-runtime/src/interrupts.js.map +1 -1
- package/dist/packages/agent-runtime/src/messages.cjs +70 -14
- package/dist/packages/agent-runtime/src/messages.cjs.map +1 -1
- package/dist/packages/agent-runtime/src/messages.js +71 -15
- package/dist/packages/agent-runtime/src/messages.js.map +1 -1
- package/dist/packages/agent-runtime/src/tool-metadata.cjs +75 -0
- package/dist/packages/agent-runtime/src/tool-metadata.cjs.map +1 -1
- package/dist/packages/agent-runtime/src/tool-metadata.js +75 -0
- package/dist/packages/agent-runtime/src/tool-metadata.js.map +1 -1
- package/dist/packages/agent-runtime/src/tool-result.cjs +19 -4
- package/dist/packages/agent-runtime/src/tool-result.cjs.map +1 -1
- package/dist/packages/agent-runtime/src/tool-result.js +19 -4
- package/dist/packages/agent-runtime/src/tool-result.js.map +1 -1
- package/dist/packages/agent-runtime/src/use-deep-agent-thread.cjs +23 -4
- package/dist/packages/agent-runtime/src/use-deep-agent-thread.cjs.map +1 -1
- package/dist/packages/agent-runtime/src/use-deep-agent-thread.js +24 -5
- package/dist/packages/agent-runtime/src/use-deep-agent-thread.js.map +1 -1
- package/dist/packages/react/package.json.cjs +1 -1
- package/dist/packages/react/package.json.js +1 -1
- package/dist/provider/PostHogProvider.cjs +3 -1
- package/dist/provider/PostHogProvider.cjs.map +1 -1
- package/dist/provider/PostHogProvider.js +3 -1
- package/dist/provider/PostHogProvider.js.map +1 -1
- package/dist/runtime/auth.cjs +88 -4
- package/dist/runtime/auth.cjs.map +1 -1
- package/dist/runtime/auth.d.ts +72 -5
- package/dist/runtime/auth.js +88 -4
- package/dist/runtime/auth.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -106,7 +106,7 @@ function App() {
|
|
|
106
106
|
|
|
107
107
|
If `config.appUrl` is omitted, the provider resolves it from the parent bridge or from the matching Athena environment defaults. Known Athena staging and production `apiUrl` and `backendUrl` values automatically fall back to the corresponding frontend origin.
|
|
108
108
|
|
|
109
|
-
Use `trustedParentOrigins` when the SDK runs inside an iframe on a private-cloud or custom parent domain and you want to explicitly allow postMessage auth/config from that parent.
|
|
109
|
+
Use `trustedParentOrigins` when the SDK runs inside an iframe on a private-cloud or custom parent domain and you want to explicitly allow postMessage auth/config from that parent. Only the window that frames the app is heard. By default `athenaintel.com` hosts, localhost and your own origin are trusted, except the hosts that serve customer-written code (`*.app.athenaintel.com`, `*.code-sandpack.athenaintel.com` and friends; see "What's new in 0.28.0").
|
|
110
110
|
|
|
111
111
|
## Transport (statewire only, as of 0.18.0)
|
|
112
112
|
|
|
@@ -470,6 +470,22 @@ Both use the same bearer / API key as session creation. The full recipe
|
|
|
470
470
|
template skill `voice-session-guide.md`. `createAthenaVoiceSession` is also
|
|
471
471
|
exported if you hold the bridge state yourself.
|
|
472
472
|
|
|
473
|
+
## What's new in 0.28.0
|
|
474
|
+
|
|
475
|
+
**The iframe auth bridge only listens to the page that frames your app, and that page can no longer point the SDK's token at another server.** When the SDK runs inside an iframe, `useParentBridge` takes `athena-auth` (a token) and `athena-config` (URLs) from the parent window. Before this release it took them from any window on any `*.athenaintel.com` origin, including the hosts that serve customer computer and application apps, and an `athena-config` could replace `backendBaseUrl` / `statewireSyncUrl` at any time. A page on one of those hosts could frame or open another app and make its SDK send its token to a server of its choosing. Now:
|
|
476
|
+
|
|
477
|
+
- A message counts only when its `source` is the window that frames the app (`window.parent`). A popup, an opener or a sibling frame on a trusted origin is ignored.
|
|
478
|
+
- Hosts that serve customer-written code are no longer trusted parents by default: Marathon's computer and application apps (`{id}.app.athenaintel.com`, `{id}.staging-app.athenaintel.com`, `{id}.canary-app.athenaintel.com`, `{id}.app.{env}.athenaintel.com`, `{id}.computer.stg.athenaintel.com`, `{id}.computer-canary.stg.athenaintel.com`) and User Interface previews (`{id}.code-sandpack.athenaintel.com`). Every other `athenaintel.com` host (Olympus on `app`, `staging-app` and `canary-app.athenaintel.com` and its previews among them), localhost, your own origin and every origin you list in `trustedParentOrigins` are trusted as before.
|
|
479
|
+
- A URL in `athena-config` (`backendBaseUrl`, `statewireSyncUrl`, `appUrl`) is taken only when it is `https:` (plain `http:` only between `localhost`, `127.0.0.1` and `[::1]`), carries no credentials, is not one of the customer-code hosts above, and is either an Athena host (`athenaintel.com`) or, for a parent outside `athenaintel.com`, the parent's own host or a subdomain of it (`portal.example.com` → `portal.example.com`, `api.portal.example.com`). Sibling hosts are refused: without a public-suffix list `example.co.uk` or `tenant.github.io` cannot be told apart from a private domain.
|
|
480
|
+
- The first `athena-config` sets the URLs. A later one cannot change a URL or add one the first left out, so a running SDK cannot be repointed. Resending the same values is fine.
|
|
481
|
+
- A refused URL emits the new diagnostics event `auth.bridge.config_rejected` (level `warn`, with `field` and `reason`: `insecure`, `other_domain`, `customer_app_host`, `invalid` or `changed`). When the refused URL is the statewire mount, the provider stops with the existing "without a statewire sync mount" error, which now names that cause.
|
|
482
|
+
|
|
483
|
+
`useQuoteFromPostMessage` (mounted by `AthenaChat`) now stages `athena-add-to-composer` text only from a frame embedded directly in the page. Any origin is still accepted, but a popup, an opener or another tab can no longer stage text.
|
|
484
|
+
|
|
485
|
+
The `/_athena/auth` mint (an SDK app served top-level by Marathon) is unchanged.
|
|
486
|
+
|
|
487
|
+
**Host changes:** none for Olympus or a Marathon wrapper. A wrapper whose page lives on a customer-code host passes its origin in `trustedParentOrigins`; one whose backend is neither an Athena host nor under its own domain (or that runs locally on a host other than the loopback names above) passes its URLs through `config` (`backendUrl`, `statewireSyncUrl`, `appUrl`). Explicit config always wins over the bridge.
|
|
488
|
+
|
|
473
489
|
## What's new in 0.26.6
|
|
474
490
|
|
|
475
491
|
**Queued messages read like the message you sent.** In the `StatewireQueuedMessages` tray (mounted by `AthenaChat`), an `[@name](…)` mention renders as a mention chip instead of the raw markdown link, and a long message clamps to two lines behind a Show more / Show less toggle. A host that passes `renderContent` keeps its own renderer.
|
|
@@ -302,6 +302,24 @@ const StatewireApprovalCardInner = ({
|
|
|
302
302
|
}
|
|
303
303
|
);
|
|
304
304
|
};
|
|
305
|
+
const ApprovalContextFacts = ({ context }) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-1 space-y-0.5", "data-testid": "statewire-approval-context", children: [
|
|
306
|
+
context.facts.map((fact) => /* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
|
|
307
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-semibold", children: [
|
|
308
|
+
fact.label,
|
|
309
|
+
":"
|
|
310
|
+
] }),
|
|
311
|
+
" ",
|
|
312
|
+
fact.value
|
|
313
|
+
] }, fact.label)),
|
|
314
|
+
context.warnings.map((warning) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
315
|
+
"p",
|
|
316
|
+
{
|
|
317
|
+
className: warning.severity === "info" ? void 0 : "font-semibold",
|
|
318
|
+
children: warning.message
|
|
319
|
+
},
|
|
320
|
+
warning.message
|
|
321
|
+
))
|
|
322
|
+
] });
|
|
305
323
|
const DefaultApprovalDetails = ({
|
|
306
324
|
actionRequests
|
|
307
325
|
}) => /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: actionRequests.map((action, index) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
@@ -310,6 +328,7 @@ const DefaultApprovalDetails = ({
|
|
|
310
328
|
className: "rounded-lg bg-amber-100 p-2 text-amber-900 text-xs leading-relaxed",
|
|
311
329
|
children: [
|
|
312
330
|
actionRequests.length > 1 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-mono font-semibold", children: action.name }) : null,
|
|
331
|
+
action.approvalContext ? /* @__PURE__ */ jsxRuntime.jsx(ApprovalContextFacts, { context: action.approvalContext }) : null,
|
|
313
332
|
Object.keys(action.args).length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("pre", { className: "mt-1 max-h-32 overflow-auto whitespace-pre-wrap break-words rounded bg-white/60 p-1.5 font-mono text-[11px]", children: JSON.stringify(action.args, null, 2) }) : /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-amber-800", children: "No arguments." })
|
|
314
333
|
]
|
|
315
334
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StatewireApprovalCard.cjs","sources":["../../src/chat/StatewireApprovalCard.tsx"],"sourcesContent":["import { useAuiState } from '@assistant-ui/react';\nimport type { StatewireThread } from '@assistant-ui/react-statewire';\nimport {\n actionRequestToolCallIds,\n canApproveForSession,\n fanLaneDecision,\n HITL_PENDING_RESUME_GRACE_MS,\n hasStatewireThreadExtras,\n type HitlActionDecision,\n type HitlApprovalPayload,\n hitlResumeActionDecisions,\n hitlResumeApprove,\n hitlResumeApproveForSession,\n hitlResumeReject,\n interruptResumeDecline,\n laneInterruptIds,\n type PendingActionRequest,\n readInterruptMessage,\n} from '@athenaintel/agent-runtime';\nimport {\n CheckCheckIcon,\n CheckIcon,\n ChevronDownIcon,\n TriangleAlertIcon,\n XIcon,\n} from 'lucide-react';\nimport {\n type FC,\n type ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport { cn } from '../lib/utils';\nimport { Button } from '../ui/button';\nimport {\n type StatewireInterruptPredicate,\n useStatewireApprovalRequest,\n} from './use-statewire-approval-request';\n\n/**\n * Human-in-the-loop approval card for the statewire transport — the one\n * approval UI every Athena chat surface mounts (the SDK's `AthenaChat`, the\n * Chrome side panel, the Office task pane, Olympus).\n *\n * When the deep-agent runtime hits an approval-gated tool call, the backend\n * parks the run on an `interrupt` input request. This card reads that request\n * through `useStatewireApprovalRequest` and answers it with\n * `{ type: 'run/input', requestId, response: { type: 'resume', value } }` —\n * the exact command the backend's resume path consumes. Without an answer the\n * run stays parked forever, so this card must be mounted on every statewire\n * chat surface. On legacy (non-statewire) threads it renders nothing.\n *\n * A gated call raised inside a delegated subagent parks only its lane: the\n * host mirrors that interrupt while the sibling lanes keep streaming, before\n * any input request exists. The card renders the mirrored payload the same\n * way and answers it with `run/lane-input` keyed by the lane's langgraph\n * interrupt id (`laneInterruptIds`); the host queues the decision and applies\n * it the moment the run parks, so the approver never waits on the siblings.\n * Once parked, a payload whose calls all carry interrupt ids resumes as a\n * `lane_decisions` map too — one positional `run/input` can only answer the\n * first of several parked lanes.\n *\n * Recovery of a queued decision the host never applies is ordered host\n * first: the host's answerer resumes the park or drops (and flushes) the\n * queue within `QUEUED_LANE_ANSWER_BUDGET_SECONDS` of the park, and this\n * card's grace (`HITL_PENDING_RESUME_GRACE_MS`, longer than that budget)\n * fires only when the host stayed silent. When it fires, the card that sent\n * the queued decision asks the host to replay it (`run/lane-replay` — the\n * host resumes as the approver, under the meta it recorded, and refuses any\n * other caller); every other card, and a replay the host refuses, hands the\n * decision back to a person by unlocking the actions. A queued decision is\n * never re-sent as a `run/input` of the viewing client's own: that would run\n * the approver's decision under the viewer's identity and configuration.\n *\n * Host-specific review UIs (subagent cards, email summaries) slot in through\n * `renderDetails`; a host composer that must also OWN the decision (an\n * editable email whose edits have to reach the backend) takes over the\n * details and the decision row through `renderEditor`; requests another\n * surface answers (typed interrupt families, per-lane subagent approvals)\n * stand down through `isHandledElsewhere`; viewers without edit rights see\n * `readOnlyNotice`.\n */\n\n/** What the card reads in place of the host's queued lanes once it has given up on them. */\nconst NO_HOST_QUEUED_LANE_DECISIONS: Readonly<Record<string, unknown>> = Object.freeze({});\n\n/** The pending request a host slot renders or judges. */\nexport interface StatewireApprovalRequestContext {\n /** The parked input request — `undefined` while the run still streams (a live lane approval). */\n request: StatewireThread.CustomInputRequest | undefined;\n /** The payload narrowed to the Athena HITL approval shape, or `null` for a generic park. */\n hitl: HitlApprovalPayload | null;\n /** Every gated call the approval covers, in decision order (empty for non-HITL parks). */\n actionRequests: PendingActionRequest[];\n}\n\n/** The request a host editor renders, plus the decisions it may send for it. */\nexport interface StatewireApprovalEditorContext extends StatewireApprovalRequestContext {\n /** A decision is in flight (or a live lane already answered); the editor must not send another. */\n locked: boolean;\n /** Whether \"approve all for this session\" may be offered for this batch. */\n sessionApprovalOffered: boolean;\n approve: () => void;\n approveForSession: () => void;\n reject: (reason?: string) => void;\n /**\n * Answer each gated call on its own — the only way an EDITED call reaches\n * the backend. `decision` is asked once per call, in order, so the answer\n * always names exactly the parked calls (a partial map is refused\n * server-side and nothing runs). `null` when the batch cannot take\n * per-action answers: a call without a `tool_call_id`, or a live / multi-\n * lane approval, which resumes per lane and carries no edit.\n */\n decideEach: ((decision: (request: PendingActionRequest) => HitlActionDecision) => void) | null;\n}\n\nexport interface StatewireApprovalCardProps {\n /** Surface-styling overrides merged onto the default classes. */\n classNames?: {\n /** The approval card container. */\n root?: string;\n /** The \"approval was cancelled\" notice container. */\n abandoned?: string;\n /** The disclosed details block. */\n details?: string;\n /** The decision row. */\n actions?: string;\n };\n /** Copy shown when an interrupt payload carries no message. */\n fallbackMessage?: string;\n /**\n * Which pending interrupt this card answers. Defaults to\n * `isApprovalCardInterrupt` (client-tool parks excluded — keep it that way\n * unless the surface handles those itself).\n */\n interruptPredicate?: StatewireInterruptPredicate;\n /**\n * How long a clicked decision keeps the buttons locked when no resume run\n * ever starts (a `run/input` the host rejected before any run began).\n * Defaults to `HITL_PENDING_RESUME_GRACE_MS`.\n */\n pendingResumeGraceMs?: number;\n /**\n * Return true when another surface answers this request (a typed Athena\n * interrupt family, per-lane subagent approval cards). The card renders\n * nothing for it — two surfaces must never answer the same input request.\n */\n isHandledElsewhere?: (context: StatewireApprovalRequestContext) => boolean;\n /**\n * Replace the default argument disclosure (one JSON block per gated call)\n * with a host-specific review UI. Rendered inside the collapsible details\n * region; the decision buttons stay with the card.\n */\n renderDetails?: (context: StatewireApprovalRequestContext) => ReactNode;\n /**\n * Replace BOTH the details region and the decision row with a host\n * composer that decides the request itself through the context's\n * callbacks (an editable email whose edits must be what runs). Return\n * `null`/`undefined` to keep the default card for this request. Not\n * consulted for read-only viewers or non-HITL parks.\n */\n renderEditor?: (context: StatewireApprovalEditorContext) => ReactNode;\n /**\n * The viewer may see the request (it explains why the run is parked) but\n * cannot decide it — answering steers the shared run. Renders\n * `readOnlyNotice` in place of the buttons.\n */\n readOnly?: boolean;\n readOnlyNotice?: ReactNode;\n /**\n * Copy for a park that is not a HITL approval — a typed Athena interrupt\n * this surface has no card for. The only honest action is to decline it\n * (`{ action: 'reject' }`, which every typed raiser treats as a clean user\n * decline); a generic \"Continue\" would be rejected by the raiser's resume\n * schema and surface as a tool error.\n */\n nonHitlNotice?: ReactNode;\n /**\n * The host refused a decision (or the transport lost it). The optimistic\n * fold has rolled back and the request is on screen again with its actions\n * re-enabled; this is where a host tells the user why (a toast, a log line).\n */\n onResumeRejected?: (error: unknown, requestId: string) => void;\n}\n\nexport const StatewireApprovalCard: FC<StatewireApprovalCardProps> = (props) => {\n const extras = useAuiState((s) => s.thread.extras);\n if (!hasStatewireThreadExtras(extras)) return null;\n return <StatewireApprovalCardInner {...props} />;\n};\n\nconst StatewireApprovalCardInner: FC<StatewireApprovalCardProps> = ({\n classNames,\n fallbackMessage,\n interruptPredicate,\n pendingResumeGraceMs,\n isHandledElsewhere,\n renderDetails,\n renderEditor,\n readOnly = false,\n readOnlyNotice = 'Waiting for someone with edit access to respond.',\n nonHitlNotice = 'This request needs an answer this surface cannot collect.',\n onResumeRejected,\n}) => {\n const {\n request,\n requestId,\n hitl: parkedHitl,\n actionRequests: parkedActionRequests,\n liveHitl,\n liveActionRequests,\n queuedLaneDecisions,\n abandonedInterrupt,\n pending,\n resume,\n sendLaneDecisions,\n replayQueuedLanes,\n } = useStatewireApprovalRequest<StatewireThread.InputResponse>({\n interruptPredicate,\n pendingResumeGraceMs,\n onResumeRejected,\n });\n const [dismissedId, setDismissedId] = useState<string>();\n // The card shares the chat column with the thread. A lone gated call is\n // small enough to show inline, but a fan-out of a dozen calls would swallow\n // the conversation, so a batch starts collapsed until someone opens it.\n const [detailsOverride, setDetailsOverride] = useState<boolean>();\n // The live lanes a decision was sent for (joined ids); the buttons stay\n // locked until the host echoes them in `queuedLaneDecisions` or refuses.\n const [liveSentKey, setLiveSentKey] = useState<string>();\n // The parked request whose host-queued lanes outlived the grace window\n // (see `queuedLanesAwaitingHost` below): its queue is read as stale.\n const [staleQueuedRequestId, setStaleQueuedRequestId] = useState<string>();\n // The parked request whose queued lanes this card has asked the host to\n // replay (`run/lane-replay` in flight): locked until the host answers.\n const [replayingRequestId, setReplayingRequestId] = useState<string>();\n // The lanes THIS card decided live (its own `run/lane-input` the host\n // accepted). Only a queue this card recorded is replayed from here — the\n // host refuses any other caller, and a second editor's card must not even\n // ask. Pruned to the lanes on screen whenever a different view arrives, so\n // the record survives the live → parked transition of the same lanes (and\n // a fold that briefly shows nothing) and dies with a new request.\n const sentLaneIdsRef = useRef<Set<string>>(new Set());\n // The parked request already replayed once from here: a request is\n // replayed at most once, then the grace hands the decision back.\n const replayedRequestIdRef = useRef<string>(undefined);\n\n const isLive = request === undefined && liveHitl !== null;\n const hitl = isLive ? liveHitl : parkedHitl;\n const actionRequests = isLive ? liveActionRequests : parkedActionRequests;\n const laneIds = laneInterruptIds(actionRequests);\n // The host answers the lanes it queued from its own settle path, within its\n // published budget of the park (`QUEUED_LANE_ANSWER_BUDGET_SECONDS`). A\n // parked request whose queued lanes are still unanswered past the grace\n // window has a silent answerer, and either this card asked the host to\n // replay them and was refused, or the queue is not this card's to replay —\n // so the queue no longer counts as a decision here: the actions re-enable\n // and the next click answers the parked request itself (a resume the host\n // accepts, and whose dispatch drops the stale queue).\n const hostQueuedLaneDecisions =\n requestId !== undefined && staleQueuedRequestId === requestId\n ? NO_HOST_QUEUED_LANE_DECISIONS\n : queuedLaneDecisions;\n const undecidedLaneIds =\n laneIds?.filter((id) => !(id in hostQueuedLaneDecisions)) ?? null;\n // A lone parked lane (or an unstamped batch) answers with one positional\n // `run/input`; that is the only resume the backend binds per-action answers\n // to, so it is the only place an edited call can go.\n const positional = laneIds === null || (!isLive && laneIds.length <= 1);\n const liveKey = isLive ? (laneIds?.join('|') ?? null) : null;\n const viewKey = requestId ?? liveKey ?? undefined;\n\n // A fresh approval gets the default for ITS batch size, not the disclosure\n // the previous one was left in. Reset during render via React's \"previous\n // value\" pattern.\n const prevViewKeyRef = useRef(viewKey);\n if (prevViewKeyRef.current !== viewKey) {\n prevViewKeyRef.current = viewKey;\n if (detailsOverride !== undefined) setDetailsOverride(undefined);\n if (liveSentKey !== undefined) setLiveSentKey(undefined);\n if (laneIds !== null) {\n for (const id of sentLaneIdsRef.current) {\n if (!laneIds.includes(id)) sentLaneIdsRef.current.delete(id);\n }\n }\n }\n // The per-request marks (stale queue, replayed once, replay in flight)\n // outlive the optimistic fold of a click's own `run/input`: the request\n // leaves and comes back under the SAME id when the host refuses it, and a\n // card that forgot its stale mark then would relock over the standing\n // queue for another grace, and could replay it a second time. They are\n // forgotten only for a genuinely different parked request.\n if (\n requestId !== undefined &&\n staleQueuedRequestId !== undefined &&\n requestId !== staleQueuedRequestId\n ) {\n setStaleQueuedRequestId(undefined);\n }\n if (\n requestId !== undefined &&\n replayingRequestId !== undefined &&\n requestId !== replayingRequestId\n ) {\n setReplayingRequestId(undefined);\n }\n if (\n requestId !== undefined &&\n replayedRequestIdRef.current !== undefined &&\n requestId !== replayedRequestIdRef.current\n ) {\n replayedRequestIdRef.current = undefined;\n }\n\n const context: StatewireApprovalRequestContext = { request, hitl, actionRequests };\n const handledElsewhere = isHandledElsewhere?.(context) ?? false;\n const replaying = replayingRequestId !== undefined && replayingRequestId === requestId;\n\n // Parked, every lane the request covers already queued on the host, no\n // resume of our own in flight, and this card is the one that would decide\n // (not a viewer, not a request another surface owns): the host's answerer\n // is expected to resume the run within its budget. Start the clock on it,\n // longer than that budget. When it runs out the host has stayed silent: if\n // this card queued those lanes itself, it asks the host to replay them as\n // their approver (`run/lane-replay`); otherwise — and when the host refuses\n // the replay — the queue no longer counts as a decision here and the actions\n // re-enable, so a person decides. Never a `run/input` of this client's own\n // carrying someone else's recorded decision.\n const queuedLanesAwaitingHost =\n !isLive &&\n !readOnly &&\n !handledElsewhere &&\n requestId !== undefined &&\n undecidedLaneIds !== null &&\n undecidedLaneIds.length === 0 &&\n !pending &&\n !replaying;\n const queuedLaneGraceMs = pendingResumeGraceMs ?? HITL_PENDING_RESUME_GRACE_MS;\n const laneKey = laneIds?.join('|');\n const onResumeRejectedRef = useRef(onResumeRejected);\n onResumeRejectedRef.current = onResumeRejected;\n useEffect(() => {\n if (!queuedLanesAwaitingHost || requestId === undefined || laneKey === undefined) {\n return undefined;\n }\n const timer = setTimeout(() => {\n const ownQueue =\n laneKey.split('|').every((id) => sentLaneIdsRef.current.has(id)) &&\n replayedRequestIdRef.current !== requestId;\n if (!ownQueue) {\n setStaleQueuedRequestId(requestId);\n return;\n }\n replayedRequestIdRef.current = requestId;\n setReplayingRequestId(requestId);\n replayQueuedLanes(requestId).then(\n () => {\n setReplayingRequestId((current) => (current === requestId ? undefined : current));\n },\n (error: unknown) => {\n setReplayingRequestId((current) => (current === requestId ? undefined : current));\n setStaleQueuedRequestId(requestId);\n onResumeRejectedRef.current?.(error, requestId);\n },\n );\n }, queuedLaneGraceMs);\n return () => clearTimeout(timer);\n }, [queuedLanesAwaitingHost, requestId, laneKey, queuedLaneGraceMs, replayQueuedLanes]);\n\n const decide = useCallback(\n (value: Record<string, string>) => {\n // Several parked lanes (or a live one) answer by interrupt id: a\n // positional `run/input` would resume only the first and re-park the\n // rest. A lone parked lane keeps the positional resume every host\n // already speaks.\n if (positional || undecidedLaneIds === null) {\n resume(value);\n return;\n }\n const lanes = fanLaneDecision({ interruptIds: undecidedLaneIds, decision: value });\n if (!isLive) {\n sendLaneDecisions(lanes);\n return;\n }\n const key = undecidedLaneIds.join('|');\n setLiveSentKey(key);\n Promise.resolve(sendLaneDecisions(lanes)).then(\n () => {\n // The host holds these decisions for this card's caller: they are\n // the only queue this card may ask it to replay.\n for (const id of undecidedLaneIds) sentLaneIdsRef.current.add(id);\n },\n (error: unknown) => {\n setLiveSentKey((current) => (current === key ? undefined : current));\n onResumeRejected?.(error, undecidedLaneIds[0] ?? '');\n },\n );\n },\n [positional, undecidedLaneIds, isLive, resume, sendLaneDecisions, onResumeRejected],\n );\n\n const decideEach = useMemo<StatewireApprovalEditorContext['decideEach']>(() => {\n if (actionRequestToolCallIds(actionRequests) === null || !positional) return null;\n return (decision) => {\n const decisions: Record<string, HitlActionDecision> = {};\n for (const request of actionRequests) {\n if (request.toolCallId !== undefined) decisions[request.toolCallId] = decision(request);\n }\n // The edited argument set comes from the wire as JSON; the resume value\n // is that JSON going back.\n resume(hitlResumeActionDecisions(decisions) as StatewireThread.InputResponse);\n };\n }, [actionRequests, positional, resume]);\n\n if (!request && !isLive) {\n if (!abandonedInterrupt || abandonedInterrupt.id === dismissedId) return null;\n return (\n <div\n className={cn(\n 'aui-approval-abandoned flex w-full items-center justify-between gap-3 rounded-xl border border-amber-300 bg-amber-50 px-3 py-2 text-amber-800 text-xs',\n classNames?.abandoned,\n )}\n >\n <p>Approval was cancelled — the run moved on.</p>\n <button\n type=\"button\"\n aria-label=\"Dismiss cancelled approval\"\n onClick={() => setDismissedId(abandonedInterrupt.id)}\n className=\"shrink-0 rounded-md p-1 hover:bg-amber-100\"\n >\n <XIcon className=\"size-3.5\" />\n </button>\n </div>\n );\n }\n\n if (handledElsewhere) return null;\n // A live mirror without lane stamps cannot be answered before the run\n // parks; the parked request renders it then.\n if (isLive && laneIds === null) return null;\n\n const message = readInterruptMessage(request ? request.payload : liveHitl, fallbackMessage);\n const toolName = hitl?.context?.tool_name ?? hitl?.context?.tool_id;\n const pendingCount = hitl?.context?.pending_action_count ?? 1;\n const hasDetails = hitl !== null && actionRequests.length > 0;\n const showDetails = hasDetails && (detailsOverride ?? actionRequests.length <= 1);\n const sessionApprovalOffered = hitl !== null && canApproveForSession(actionRequests);\n const laneDecided =\n undecidedLaneIds !== null &&\n laneIds !== null &&\n (undecidedLaneIds.length === 0 || liveSentKey === undecidedLaneIds.join('|'));\n const locked = pending || laneDecided || replaying;\n\n const editor =\n !readOnly && hitl !== null && renderEditor\n ? renderEditor({\n ...context,\n locked,\n sessionApprovalOffered,\n approve: () => decide(hitlResumeApprove()),\n approveForSession: () => decide(hitlResumeApproveForSession()),\n reject: (reason) => decide(hitlResumeReject(reason)),\n decideEach,\n })\n : undefined;\n\n return (\n <section\n aria-label={hitl ? 'Approval required' : 'Input required'}\n className={cn(\n 'aui-approval-card w-full rounded-2xl border border-amber-300 bg-amber-50 px-4 py-3 text-amber-900',\n classNames?.root,\n )}\n >\n <div className=\"flex items-start gap-3\">\n <TriangleAlertIcon className=\"mt-0.5 size-4 shrink-0\" />\n <div className=\"min-w-0 flex-1\">\n <p className=\"flex items-center gap-2 font-semibold text-sm\">\n <span>{hitl ? 'Approval required' : 'Waiting for input'}</span>\n {hitl && pendingCount > 1 ? (\n <span className=\"shrink-0 font-normal text-amber-800 text-xs\">\n {pendingCount} tool calls\n </span>\n ) : null}\n </p>\n <p className=\"pt-1 text-amber-800 text-sm leading-snug\">\n {hitl && pendingCount === 1 && toolName ? (\n <>\n <span className=\"font-mono\">{toolName}</span> requires approval before it runs.\n </>\n ) : (\n message\n )}\n </p>\n {isLive ? (\n <p className=\"pt-1 text-amber-700 text-xs\" data-testid=\"statewire-approval-live\">\n {laneDecided\n ? 'Decision sent — it applies as soon as this step pauses.'\n : 'Other work keeps running while you decide.'}\n </p>\n ) : null}\n {hasDetails && editor == null ? (\n <button\n type=\"button\"\n onClick={() => setDetailsOverride(!showDetails)}\n aria-expanded={showDetails}\n className=\"mt-2 flex items-center gap-1 rounded px-1 py-0.5 text-amber-800 text-xs transition-colors hover:text-amber-950\"\n >\n <ChevronDownIcon\n className={cn('size-3 transition-transform', showDetails && 'rotate-180')}\n />\n {showDetails ? 'Hide details' : 'Review details'}\n </button>\n ) : null}\n {/* EVERY pending gated call renders, in order — one decision\n authorizes the whole batch, so nothing may hide behind a host's\n richer cards. All read-only: this runtime resumes through\n `hitl_response_to_decisions`, which has no `tool_args` channel —\n an editable composer would let someone rewrite an email and then\n watch the original go out — unless the host takes over the\n decision too (`renderEditor`), which sends the edit per call. */}\n {editor != null ? (\n <div className={cn('mt-3 flex flex-col gap-2', classNames?.details)}>{editor}</div>\n ) : showDetails ? (\n <div className={cn('mt-2 flex flex-col gap-2', classNames?.details)}>\n {renderDetails ? (\n renderDetails(context)\n ) : (\n <DefaultApprovalDetails actionRequests={actionRequests} />\n )}\n </div>\n ) : null}\n </div>\n </div>\n {editor != null ? null : (\n <div className={cn('flex flex-wrap items-center gap-2 pt-3', classNames?.actions)}>\n {readOnly ? (\n <p className=\"text-amber-800 text-sm\" data-testid=\"statewire-approval-read-only\">\n {readOnlyNotice}\n </p>\n ) : hitl ? (\n <>\n <Button\n size=\"sm\"\n disabled={locked}\n onClick={() => decide(hitlResumeApprove())}\n className=\"bg-green-600 text-white hover:bg-green-700\"\n >\n <CheckIcon />\n Approve\n </Button>\n {sessionApprovalOffered ? (\n <Button\n size=\"sm\"\n variant=\"outline\"\n disabled={locked}\n onClick={() => decide(hitlResumeApproveForSession())}\n data-testid=\"statewire-approval-approve-session\"\n >\n <CheckCheckIcon />\n Approve all for this session\n </Button>\n ) : null}\n <Button\n size=\"sm\"\n variant=\"outline\"\n disabled={locked}\n onClick={() => decide(hitlResumeReject())}\n >\n <XIcon />\n Reject\n </Button>\n </>\n ) : (\n <>\n <p className=\"text-amber-800 text-sm\" data-testid=\"statewire-approval-non-hitl\">\n {nonHitlNotice}\n </p>\n <Button\n size=\"sm\"\n variant=\"outline\"\n disabled={pending}\n onClick={() => resume(interruptResumeDecline())}\n >\n <XIcon />\n Decline\n </Button>\n </>\n )}\n </div>\n )}\n </section>\n );\n};\n\n/** One block per gated call: its name (when the batch has several) and its arguments as JSON. */\nconst DefaultApprovalDetails: FC<{ actionRequests: PendingActionRequest[] }> = ({\n actionRequests,\n}) => (\n <>\n {actionRequests.map((action, index) => (\n <div\n key={`${action.name}-${index}`}\n className=\"rounded-lg bg-amber-100 p-2 text-amber-900 text-xs leading-relaxed\"\n >\n {actionRequests.length > 1 ? (\n <p className=\"font-mono font-semibold\">{action.name}</p>\n ) : null}\n {Object.keys(action.args).length > 0 ? (\n <pre className=\"mt-1 max-h-32 overflow-auto whitespace-pre-wrap break-words rounded bg-white/60 p-1.5 font-mono text-[11px]\">\n {JSON.stringify(action.args, null, 2)}\n </pre>\n ) : (\n <p className=\"text-amber-800\">No arguments.</p>\n )}\n </div>\n ))}\n </>\n);\n"],"names":["useAuiState","hasStatewireThreadExtras","jsx","useStatewireApprovalRequest","useState","useRef","laneInterruptIds","HITL_PENDING_RESUME_GRACE_MS","useEffect","_a","useCallback","fanLaneDecision","useMemo","actionRequestToolCallIds","request","hitlResumeActionDecisions","jsxs","cn","XIcon","readInterruptMessage","canApproveForSession","hitlResumeApprove","hitlResumeApproveForSession","hitlResumeReject","TriangleAlertIcon","Fragment","ChevronDownIcon","Button","CheckIcon","CheckCheckIcon","interruptResumeDecline"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"StatewireApprovalCard.cjs","sources":["../../src/chat/StatewireApprovalCard.tsx"],"sourcesContent":["import { useAuiState } from '@assistant-ui/react';\nimport type { StatewireThread } from '@assistant-ui/react-statewire';\nimport {\n actionRequestToolCallIds,\n canApproveForSession,\n fanLaneDecision,\n HITL_PENDING_RESUME_GRACE_MS,\n hasStatewireThreadExtras,\n type HitlActionDecision,\n type HitlApprovalPayload,\n hitlResumeActionDecisions,\n hitlResumeApprove,\n hitlResumeApproveForSession,\n hitlResumeReject,\n interruptResumeDecline,\n laneInterruptIds,\n type PendingActionRequest,\n type PendingApprovalContext,\n readInterruptMessage,\n} from '@athenaintel/agent-runtime';\nimport {\n CheckCheckIcon,\n CheckIcon,\n ChevronDownIcon,\n TriangleAlertIcon,\n XIcon,\n} from 'lucide-react';\nimport {\n type FC,\n type ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport { cn } from '../lib/utils';\nimport { Button } from '../ui/button';\nimport {\n type StatewireInterruptPredicate,\n useStatewireApprovalRequest,\n} from './use-statewire-approval-request';\n\n/**\n * Human-in-the-loop approval card for the statewire transport — the one\n * approval UI every Athena chat surface mounts (the SDK's `AthenaChat`, the\n * Chrome side panel, the Office task pane, Olympus).\n *\n * When the deep-agent runtime hits an approval-gated tool call, the backend\n * parks the run on an `interrupt` input request. This card reads that request\n * through `useStatewireApprovalRequest` and answers it with\n * `{ type: 'run/input', requestId, response: { type: 'resume', value } }` —\n * the exact command the backend's resume path consumes. Without an answer the\n * run stays parked forever, so this card must be mounted on every statewire\n * chat surface. On legacy (non-statewire) threads it renders nothing.\n *\n * A gated call raised inside a delegated subagent parks only its lane: the\n * host mirrors that interrupt while the sibling lanes keep streaming, before\n * any input request exists. The card renders the mirrored payload the same\n * way and answers it with `run/lane-input` keyed by the lane's langgraph\n * interrupt id (`laneInterruptIds`); the host queues the decision and applies\n * it the moment the run parks, so the approver never waits on the siblings.\n * Once parked, a payload whose calls all carry interrupt ids resumes as a\n * `lane_decisions` map too — one positional `run/input` can only answer the\n * first of several parked lanes.\n *\n * Recovery of a queued decision the host never applies is ordered host\n * first: the host's answerer resumes the park or drops (and flushes) the\n * queue within `QUEUED_LANE_ANSWER_BUDGET_SECONDS` of the park, and this\n * card's grace (`HITL_PENDING_RESUME_GRACE_MS`, longer than that budget)\n * fires only when the host stayed silent. When it fires, the card that sent\n * the queued decision asks the host to replay it (`run/lane-replay` — the\n * host resumes as the approver, under the meta it recorded, and refuses any\n * other caller); every other card, and a replay the host refuses, hands the\n * decision back to a person by unlocking the actions. A queued decision is\n * never re-sent as a `run/input` of the viewing client's own: that would run\n * the approver's decision under the viewer's identity and configuration.\n *\n * Host-specific review UIs (subagent cards, email summaries) slot in through\n * `renderDetails`; a host composer that must also OWN the decision (an\n * editable email whose edits have to reach the backend) takes over the\n * details and the decision row through `renderEditor`; requests another\n * surface answers (typed interrupt families, per-lane subagent approvals)\n * stand down through `isHandledElsewhere`; viewers without edit rights see\n * `readOnlyNotice`.\n */\n\n/** What the card reads in place of the host's queued lanes once it has given up on them. */\nconst NO_HOST_QUEUED_LANE_DECISIONS: Readonly<Record<string, unknown>> = Object.freeze({});\n\n/** The pending request a host slot renders or judges. */\nexport interface StatewireApprovalRequestContext {\n /** The parked input request — `undefined` while the run still streams (a live lane approval). */\n request: StatewireThread.CustomInputRequest | undefined;\n /** The payload narrowed to the Athena HITL approval shape, or `null` for a generic park. */\n hitl: HitlApprovalPayload | null;\n /** Every gated call the approval covers, in decision order (empty for non-HITL parks). */\n actionRequests: PendingActionRequest[];\n}\n\n/** The request a host editor renders, plus the decisions it may send for it. */\nexport interface StatewireApprovalEditorContext extends StatewireApprovalRequestContext {\n /** A decision is in flight (or a live lane already answered); the editor must not send another. */\n locked: boolean;\n /** Whether \"approve all for this session\" may be offered for this batch. */\n sessionApprovalOffered: boolean;\n approve: () => void;\n approveForSession: () => void;\n reject: (reason?: string) => void;\n /**\n * Answer each gated call on its own — the only way an EDITED call reaches\n * the backend. `decision` is asked once per call, in order, so the answer\n * always names exactly the parked calls (a partial map is refused\n * server-side and nothing runs). `null` when the batch cannot take\n * per-action answers: a call without a `tool_call_id`, or a live / multi-\n * lane approval, which resumes per lane and carries no edit.\n */\n decideEach: ((decision: (request: PendingActionRequest) => HitlActionDecision) => void) | null;\n}\n\nexport interface StatewireApprovalCardProps {\n /** Surface-styling overrides merged onto the default classes. */\n classNames?: {\n /** The approval card container. */\n root?: string;\n /** The \"approval was cancelled\" notice container. */\n abandoned?: string;\n /** The disclosed details block. */\n details?: string;\n /** The decision row. */\n actions?: string;\n };\n /** Copy shown when an interrupt payload carries no message. */\n fallbackMessage?: string;\n /**\n * Which pending interrupt this card answers. Defaults to\n * `isApprovalCardInterrupt` (client-tool parks excluded — keep it that way\n * unless the surface handles those itself).\n */\n interruptPredicate?: StatewireInterruptPredicate;\n /**\n * How long a clicked decision keeps the buttons locked when no resume run\n * ever starts (a `run/input` the host rejected before any run began).\n * Defaults to `HITL_PENDING_RESUME_GRACE_MS`.\n */\n pendingResumeGraceMs?: number;\n /**\n * Return true when another surface answers this request (a typed Athena\n * interrupt family, per-lane subagent approval cards). The card renders\n * nothing for it — two surfaces must never answer the same input request.\n */\n isHandledElsewhere?: (context: StatewireApprovalRequestContext) => boolean;\n /**\n * Replace the default argument disclosure (one JSON block per gated call)\n * with a host-specific review UI. Rendered inside the collapsible details\n * region; the decision buttons stay with the card.\n */\n renderDetails?: (context: StatewireApprovalRequestContext) => ReactNode;\n /**\n * Replace BOTH the details region and the decision row with a host\n * composer that decides the request itself through the context's\n * callbacks (an editable email whose edits must be what runs). Return\n * `null`/`undefined` to keep the default card for this request. Not\n * consulted for read-only viewers or non-HITL parks.\n */\n renderEditor?: (context: StatewireApprovalEditorContext) => ReactNode;\n /**\n * The viewer may see the request (it explains why the run is parked) but\n * cannot decide it — answering steers the shared run. Renders\n * `readOnlyNotice` in place of the buttons.\n */\n readOnly?: boolean;\n readOnlyNotice?: ReactNode;\n /**\n * Copy for a park that is not a HITL approval — a typed Athena interrupt\n * this surface has no card for. The only honest action is to decline it\n * (`{ action: 'reject' }`, which every typed raiser treats as a clean user\n * decline); a generic \"Continue\" would be rejected by the raiser's resume\n * schema and surface as a tool error.\n */\n nonHitlNotice?: ReactNode;\n /**\n * The host refused a decision (or the transport lost it). The optimistic\n * fold has rolled back and the request is on screen again with its actions\n * re-enabled; this is where a host tells the user why (a toast, a log line).\n */\n onResumeRejected?: (error: unknown, requestId: string) => void;\n}\n\nexport const StatewireApprovalCard: FC<StatewireApprovalCardProps> = (props) => {\n const extras = useAuiState((s) => s.thread.extras);\n if (!hasStatewireThreadExtras(extras)) return null;\n return <StatewireApprovalCardInner {...props} />;\n};\n\nconst StatewireApprovalCardInner: FC<StatewireApprovalCardProps> = ({\n classNames,\n fallbackMessage,\n interruptPredicate,\n pendingResumeGraceMs,\n isHandledElsewhere,\n renderDetails,\n renderEditor,\n readOnly = false,\n readOnlyNotice = 'Waiting for someone with edit access to respond.',\n nonHitlNotice = 'This request needs an answer this surface cannot collect.',\n onResumeRejected,\n}) => {\n const {\n request,\n requestId,\n hitl: parkedHitl,\n actionRequests: parkedActionRequests,\n liveHitl,\n liveActionRequests,\n queuedLaneDecisions,\n abandonedInterrupt,\n pending,\n resume,\n sendLaneDecisions,\n replayQueuedLanes,\n } = useStatewireApprovalRequest<StatewireThread.InputResponse>({\n interruptPredicate,\n pendingResumeGraceMs,\n onResumeRejected,\n });\n const [dismissedId, setDismissedId] = useState<string>();\n // The card shares the chat column with the thread. A lone gated call is\n // small enough to show inline, but a fan-out of a dozen calls would swallow\n // the conversation, so a batch starts collapsed until someone opens it.\n const [detailsOverride, setDetailsOverride] = useState<boolean>();\n // The live lanes a decision was sent for (joined ids); the buttons stay\n // locked until the host echoes them in `queuedLaneDecisions` or refuses.\n const [liveSentKey, setLiveSentKey] = useState<string>();\n // The parked request whose host-queued lanes outlived the grace window\n // (see `queuedLanesAwaitingHost` below): its queue is read as stale.\n const [staleQueuedRequestId, setStaleQueuedRequestId] = useState<string>();\n // The parked request whose queued lanes this card has asked the host to\n // replay (`run/lane-replay` in flight): locked until the host answers.\n const [replayingRequestId, setReplayingRequestId] = useState<string>();\n // The lanes THIS card decided live (its own `run/lane-input` the host\n // accepted). Only a queue this card recorded is replayed from here — the\n // host refuses any other caller, and a second editor's card must not even\n // ask. Pruned to the lanes on screen whenever a different view arrives, so\n // the record survives the live → parked transition of the same lanes (and\n // a fold that briefly shows nothing) and dies with a new request.\n const sentLaneIdsRef = useRef<Set<string>>(new Set());\n // The parked request already replayed once from here: a request is\n // replayed at most once, then the grace hands the decision back.\n const replayedRequestIdRef = useRef<string>(undefined);\n\n const isLive = request === undefined && liveHitl !== null;\n const hitl = isLive ? liveHitl : parkedHitl;\n const actionRequests = isLive ? liveActionRequests : parkedActionRequests;\n const laneIds = laneInterruptIds(actionRequests);\n // The host answers the lanes it queued from its own settle path, within its\n // published budget of the park (`QUEUED_LANE_ANSWER_BUDGET_SECONDS`). A\n // parked request whose queued lanes are still unanswered past the grace\n // window has a silent answerer, and either this card asked the host to\n // replay them and was refused, or the queue is not this card's to replay —\n // so the queue no longer counts as a decision here: the actions re-enable\n // and the next click answers the parked request itself (a resume the host\n // accepts, and whose dispatch drops the stale queue).\n const hostQueuedLaneDecisions =\n requestId !== undefined && staleQueuedRequestId === requestId\n ? NO_HOST_QUEUED_LANE_DECISIONS\n : queuedLaneDecisions;\n const undecidedLaneIds =\n laneIds?.filter((id) => !(id in hostQueuedLaneDecisions)) ?? null;\n // A lone parked lane (or an unstamped batch) answers with one positional\n // `run/input`; that is the only resume the backend binds per-action answers\n // to, so it is the only place an edited call can go.\n const positional = laneIds === null || (!isLive && laneIds.length <= 1);\n const liveKey = isLive ? (laneIds?.join('|') ?? null) : null;\n const viewKey = requestId ?? liveKey ?? undefined;\n\n // A fresh approval gets the default for ITS batch size, not the disclosure\n // the previous one was left in. Reset during render via React's \"previous\n // value\" pattern.\n const prevViewKeyRef = useRef(viewKey);\n if (prevViewKeyRef.current !== viewKey) {\n prevViewKeyRef.current = viewKey;\n if (detailsOverride !== undefined) setDetailsOverride(undefined);\n if (liveSentKey !== undefined) setLiveSentKey(undefined);\n if (laneIds !== null) {\n for (const id of sentLaneIdsRef.current) {\n if (!laneIds.includes(id)) sentLaneIdsRef.current.delete(id);\n }\n }\n }\n // The per-request marks (stale queue, replayed once, replay in flight)\n // outlive the optimistic fold of a click's own `run/input`: the request\n // leaves and comes back under the SAME id when the host refuses it, and a\n // card that forgot its stale mark then would relock over the standing\n // queue for another grace, and could replay it a second time. They are\n // forgotten only for a genuinely different parked request.\n if (\n requestId !== undefined &&\n staleQueuedRequestId !== undefined &&\n requestId !== staleQueuedRequestId\n ) {\n setStaleQueuedRequestId(undefined);\n }\n if (\n requestId !== undefined &&\n replayingRequestId !== undefined &&\n requestId !== replayingRequestId\n ) {\n setReplayingRequestId(undefined);\n }\n if (\n requestId !== undefined &&\n replayedRequestIdRef.current !== undefined &&\n requestId !== replayedRequestIdRef.current\n ) {\n replayedRequestIdRef.current = undefined;\n }\n\n const context: StatewireApprovalRequestContext = { request, hitl, actionRequests };\n const handledElsewhere = isHandledElsewhere?.(context) ?? false;\n const replaying = replayingRequestId !== undefined && replayingRequestId === requestId;\n\n // Parked, every lane the request covers already queued on the host, no\n // resume of our own in flight, and this card is the one that would decide\n // (not a viewer, not a request another surface owns): the host's answerer\n // is expected to resume the run within its budget. Start the clock on it,\n // longer than that budget. When it runs out the host has stayed silent: if\n // this card queued those lanes itself, it asks the host to replay them as\n // their approver (`run/lane-replay`); otherwise — and when the host refuses\n // the replay — the queue no longer counts as a decision here and the actions\n // re-enable, so a person decides. Never a `run/input` of this client's own\n // carrying someone else's recorded decision.\n const queuedLanesAwaitingHost =\n !isLive &&\n !readOnly &&\n !handledElsewhere &&\n requestId !== undefined &&\n undecidedLaneIds !== null &&\n undecidedLaneIds.length === 0 &&\n !pending &&\n !replaying;\n const queuedLaneGraceMs = pendingResumeGraceMs ?? HITL_PENDING_RESUME_GRACE_MS;\n const laneKey = laneIds?.join('|');\n const onResumeRejectedRef = useRef(onResumeRejected);\n onResumeRejectedRef.current = onResumeRejected;\n useEffect(() => {\n if (!queuedLanesAwaitingHost || requestId === undefined || laneKey === undefined) {\n return undefined;\n }\n const timer = setTimeout(() => {\n const ownQueue =\n laneKey.split('|').every((id) => sentLaneIdsRef.current.has(id)) &&\n replayedRequestIdRef.current !== requestId;\n if (!ownQueue) {\n setStaleQueuedRequestId(requestId);\n return;\n }\n replayedRequestIdRef.current = requestId;\n setReplayingRequestId(requestId);\n replayQueuedLanes(requestId).then(\n () => {\n setReplayingRequestId((current) => (current === requestId ? undefined : current));\n },\n (error: unknown) => {\n setReplayingRequestId((current) => (current === requestId ? undefined : current));\n setStaleQueuedRequestId(requestId);\n onResumeRejectedRef.current?.(error, requestId);\n },\n );\n }, queuedLaneGraceMs);\n return () => clearTimeout(timer);\n }, [queuedLanesAwaitingHost, requestId, laneKey, queuedLaneGraceMs, replayQueuedLanes]);\n\n const decide = useCallback(\n (value: Record<string, string>) => {\n // Several parked lanes (or a live one) answer by interrupt id: a\n // positional `run/input` would resume only the first and re-park the\n // rest. A lone parked lane keeps the positional resume every host\n // already speaks.\n if (positional || undecidedLaneIds === null) {\n resume(value);\n return;\n }\n const lanes = fanLaneDecision({ interruptIds: undecidedLaneIds, decision: value });\n if (!isLive) {\n sendLaneDecisions(lanes);\n return;\n }\n const key = undecidedLaneIds.join('|');\n setLiveSentKey(key);\n Promise.resolve(sendLaneDecisions(lanes)).then(\n () => {\n // The host holds these decisions for this card's caller: they are\n // the only queue this card may ask it to replay.\n for (const id of undecidedLaneIds) sentLaneIdsRef.current.add(id);\n },\n (error: unknown) => {\n setLiveSentKey((current) => (current === key ? undefined : current));\n onResumeRejected?.(error, undecidedLaneIds[0] ?? '');\n },\n );\n },\n [positional, undecidedLaneIds, isLive, resume, sendLaneDecisions, onResumeRejected],\n );\n\n const decideEach = useMemo<StatewireApprovalEditorContext['decideEach']>(() => {\n if (actionRequestToolCallIds(actionRequests) === null || !positional) return null;\n return (decision) => {\n const decisions: Record<string, HitlActionDecision> = {};\n for (const request of actionRequests) {\n if (request.toolCallId !== undefined) decisions[request.toolCallId] = decision(request);\n }\n // The edited argument set comes from the wire as JSON; the resume value\n // is that JSON going back.\n resume(hitlResumeActionDecisions(decisions) as StatewireThread.InputResponse);\n };\n }, [actionRequests, positional, resume]);\n\n if (!request && !isLive) {\n if (!abandonedInterrupt || abandonedInterrupt.id === dismissedId) return null;\n return (\n <div\n className={cn(\n 'aui-approval-abandoned flex w-full items-center justify-between gap-3 rounded-xl border border-amber-300 bg-amber-50 px-3 py-2 text-amber-800 text-xs',\n classNames?.abandoned,\n )}\n >\n <p>Approval was cancelled — the run moved on.</p>\n <button\n type=\"button\"\n aria-label=\"Dismiss cancelled approval\"\n onClick={() => setDismissedId(abandonedInterrupt.id)}\n className=\"shrink-0 rounded-md p-1 hover:bg-amber-100\"\n >\n <XIcon className=\"size-3.5\" />\n </button>\n </div>\n );\n }\n\n if (handledElsewhere) return null;\n // A live mirror without lane stamps cannot be answered before the run\n // parks; the parked request renders it then.\n if (isLive && laneIds === null) return null;\n\n const message = readInterruptMessage(request ? request.payload : liveHitl, fallbackMessage);\n const toolName = hitl?.context?.tool_name ?? hitl?.context?.tool_id;\n const pendingCount = hitl?.context?.pending_action_count ?? 1;\n const hasDetails = hitl !== null && actionRequests.length > 0;\n const showDetails = hasDetails && (detailsOverride ?? actionRequests.length <= 1);\n const sessionApprovalOffered = hitl !== null && canApproveForSession(actionRequests);\n const laneDecided =\n undecidedLaneIds !== null &&\n laneIds !== null &&\n (undecidedLaneIds.length === 0 || liveSentKey === undecidedLaneIds.join('|'));\n const locked = pending || laneDecided || replaying;\n\n const editor =\n !readOnly && hitl !== null && renderEditor\n ? renderEditor({\n ...context,\n locked,\n sessionApprovalOffered,\n approve: () => decide(hitlResumeApprove()),\n approveForSession: () => decide(hitlResumeApproveForSession()),\n reject: (reason) => decide(hitlResumeReject(reason)),\n decideEach,\n })\n : undefined;\n\n return (\n <section\n aria-label={hitl ? 'Approval required' : 'Input required'}\n className={cn(\n 'aui-approval-card w-full rounded-2xl border border-amber-300 bg-amber-50 px-4 py-3 text-amber-900',\n classNames?.root,\n )}\n >\n <div className=\"flex items-start gap-3\">\n <TriangleAlertIcon className=\"mt-0.5 size-4 shrink-0\" />\n <div className=\"min-w-0 flex-1\">\n <p className=\"flex items-center gap-2 font-semibold text-sm\">\n <span>{hitl ? 'Approval required' : 'Waiting for input'}</span>\n {hitl && pendingCount > 1 ? (\n <span className=\"shrink-0 font-normal text-amber-800 text-xs\">\n {pendingCount} tool calls\n </span>\n ) : null}\n </p>\n <p className=\"pt-1 text-amber-800 text-sm leading-snug\">\n {hitl && pendingCount === 1 && toolName ? (\n <>\n <span className=\"font-mono\">{toolName}</span> requires approval before it runs.\n </>\n ) : (\n message\n )}\n </p>\n {isLive ? (\n <p className=\"pt-1 text-amber-700 text-xs\" data-testid=\"statewire-approval-live\">\n {laneDecided\n ? 'Decision sent — it applies as soon as this step pauses.'\n : 'Other work keeps running while you decide.'}\n </p>\n ) : null}\n {hasDetails && editor == null ? (\n <button\n type=\"button\"\n onClick={() => setDetailsOverride(!showDetails)}\n aria-expanded={showDetails}\n className=\"mt-2 flex items-center gap-1 rounded px-1 py-0.5 text-amber-800 text-xs transition-colors hover:text-amber-950\"\n >\n <ChevronDownIcon\n className={cn('size-3 transition-transform', showDetails && 'rotate-180')}\n />\n {showDetails ? 'Hide details' : 'Review details'}\n </button>\n ) : null}\n {/* EVERY pending gated call renders, in order — one decision\n authorizes the whole batch, so nothing may hide behind a host's\n richer cards. All read-only: this runtime resumes through\n `hitl_response_to_decisions`, which has no `tool_args` channel —\n an editable composer would let someone rewrite an email and then\n watch the original go out — unless the host takes over the\n decision too (`renderEditor`), which sends the edit per call. */}\n {editor != null ? (\n <div className={cn('mt-3 flex flex-col gap-2', classNames?.details)}>{editor}</div>\n ) : showDetails ? (\n <div className={cn('mt-2 flex flex-col gap-2', classNames?.details)}>\n {renderDetails ? (\n renderDetails(context)\n ) : (\n <DefaultApprovalDetails actionRequests={actionRequests} />\n )}\n </div>\n ) : null}\n </div>\n </div>\n {editor != null ? null : (\n <div className={cn('flex flex-wrap items-center gap-2 pt-3', classNames?.actions)}>\n {readOnly ? (\n <p className=\"text-amber-800 text-sm\" data-testid=\"statewire-approval-read-only\">\n {readOnlyNotice}\n </p>\n ) : hitl ? (\n <>\n <Button\n size=\"sm\"\n disabled={locked}\n onClick={() => decide(hitlResumeApprove())}\n className=\"bg-green-600 text-white hover:bg-green-700\"\n >\n <CheckIcon />\n Approve\n </Button>\n {sessionApprovalOffered ? (\n <Button\n size=\"sm\"\n variant=\"outline\"\n disabled={locked}\n onClick={() => decide(hitlResumeApproveForSession())}\n data-testid=\"statewire-approval-approve-session\"\n >\n <CheckCheckIcon />\n Approve all for this session\n </Button>\n ) : null}\n <Button\n size=\"sm\"\n variant=\"outline\"\n disabled={locked}\n onClick={() => decide(hitlResumeReject())}\n >\n <XIcon />\n Reject\n </Button>\n </>\n ) : (\n <>\n <p className=\"text-amber-800 text-sm\" data-testid=\"statewire-approval-non-hitl\">\n {nonHitlNotice}\n </p>\n <Button\n size=\"sm\"\n variant=\"outline\"\n disabled={pending}\n onClick={() => resume(interruptResumeDecline())}\n >\n <XIcon />\n Decline\n </Button>\n </>\n )}\n </div>\n )}\n </section>\n );\n};\n\n/**\n * What the backend resolved about a call before asking (where a Slack write\n * goes and as whom, Slack Connect warnings), above its raw arguments.\n */\nconst ApprovalContextFacts: FC<{ context: PendingApprovalContext }> = ({ context }) => (\n <div className=\"mt-1 space-y-0.5\" data-testid=\"statewire-approval-context\">\n {context.facts.map((fact) => (\n <p key={fact.label}>\n <span className=\"font-semibold\">{fact.label}:</span> {fact.value}\n </p>\n ))}\n {context.warnings.map((warning) => (\n <p\n key={warning.message}\n className={warning.severity === 'info' ? undefined : 'font-semibold'}\n >\n {warning.message}\n </p>\n ))}\n </div>\n);\n\n/** One block per gated call: its name (when the batch has several), its resolved facts, and its arguments as JSON. */\nconst DefaultApprovalDetails: FC<{ actionRequests: PendingActionRequest[] }> = ({\n actionRequests,\n}) => (\n <>\n {actionRequests.map((action, index) => (\n <div\n key={`${action.name}-${index}`}\n className=\"rounded-lg bg-amber-100 p-2 text-amber-900 text-xs leading-relaxed\"\n >\n {actionRequests.length > 1 ? (\n <p className=\"font-mono font-semibold\">{action.name}</p>\n ) : null}\n {action.approvalContext ? <ApprovalContextFacts context={action.approvalContext} /> : null}\n {Object.keys(action.args).length > 0 ? (\n <pre className=\"mt-1 max-h-32 overflow-auto whitespace-pre-wrap break-words rounded bg-white/60 p-1.5 font-mono text-[11px]\">\n {JSON.stringify(action.args, null, 2)}\n </pre>\n ) : (\n <p className=\"text-amber-800\">No arguments.</p>\n )}\n </div>\n ))}\n </>\n);\n"],"names":["useAuiState","hasStatewireThreadExtras","jsx","useStatewireApprovalRequest","useState","useRef","laneInterruptIds","HITL_PENDING_RESUME_GRACE_MS","useEffect","_a","useCallback","fanLaneDecision","useMemo","actionRequestToolCallIds","request","hitlResumeActionDecisions","jsxs","cn","XIcon","readInterruptMessage","canApproveForSession","hitlResumeApprove","hitlResumeApproveForSession","hitlResumeReject","TriangleAlertIcon","Fragment","ChevronDownIcon","Button","CheckIcon","CheckCheckIcon","interruptResumeDecline"],"mappings":"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"}
|
|
@@ -300,6 +300,24 @@ const StatewireApprovalCardInner = ({
|
|
|
300
300
|
}
|
|
301
301
|
);
|
|
302
302
|
};
|
|
303
|
+
const ApprovalContextFacts = ({ context }) => /* @__PURE__ */ jsxs("div", { className: "mt-1 space-y-0.5", "data-testid": "statewire-approval-context", children: [
|
|
304
|
+
context.facts.map((fact) => /* @__PURE__ */ jsxs("p", { children: [
|
|
305
|
+
/* @__PURE__ */ jsxs("span", { className: "font-semibold", children: [
|
|
306
|
+
fact.label,
|
|
307
|
+
":"
|
|
308
|
+
] }),
|
|
309
|
+
" ",
|
|
310
|
+
fact.value
|
|
311
|
+
] }, fact.label)),
|
|
312
|
+
context.warnings.map((warning) => /* @__PURE__ */ jsx(
|
|
313
|
+
"p",
|
|
314
|
+
{
|
|
315
|
+
className: warning.severity === "info" ? void 0 : "font-semibold",
|
|
316
|
+
children: warning.message
|
|
317
|
+
},
|
|
318
|
+
warning.message
|
|
319
|
+
))
|
|
320
|
+
] });
|
|
303
321
|
const DefaultApprovalDetails = ({
|
|
304
322
|
actionRequests
|
|
305
323
|
}) => /* @__PURE__ */ jsx(Fragment, { children: actionRequests.map((action, index) => /* @__PURE__ */ jsxs(
|
|
@@ -308,6 +326,7 @@ const DefaultApprovalDetails = ({
|
|
|
308
326
|
className: "rounded-lg bg-amber-100 p-2 text-amber-900 text-xs leading-relaxed",
|
|
309
327
|
children: [
|
|
310
328
|
actionRequests.length > 1 ? /* @__PURE__ */ jsx("p", { className: "font-mono font-semibold", children: action.name }) : null,
|
|
329
|
+
action.approvalContext ? /* @__PURE__ */ jsx(ApprovalContextFacts, { context: action.approvalContext }) : null,
|
|
311
330
|
Object.keys(action.args).length > 0 ? /* @__PURE__ */ jsx("pre", { className: "mt-1 max-h-32 overflow-auto whitespace-pre-wrap break-words rounded bg-white/60 p-1.5 font-mono text-[11px]", children: JSON.stringify(action.args, null, 2) }) : /* @__PURE__ */ jsx("p", { className: "text-amber-800", children: "No arguments." })
|
|
312
331
|
]
|
|
313
332
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StatewireApprovalCard.js","sources":["../../src/chat/StatewireApprovalCard.tsx"],"sourcesContent":["import { useAuiState } from '@assistant-ui/react';\nimport type { StatewireThread } from '@assistant-ui/react-statewire';\nimport {\n actionRequestToolCallIds,\n canApproveForSession,\n fanLaneDecision,\n HITL_PENDING_RESUME_GRACE_MS,\n hasStatewireThreadExtras,\n type HitlActionDecision,\n type HitlApprovalPayload,\n hitlResumeActionDecisions,\n hitlResumeApprove,\n hitlResumeApproveForSession,\n hitlResumeReject,\n interruptResumeDecline,\n laneInterruptIds,\n type PendingActionRequest,\n readInterruptMessage,\n} from '@athenaintel/agent-runtime';\nimport {\n CheckCheckIcon,\n CheckIcon,\n ChevronDownIcon,\n TriangleAlertIcon,\n XIcon,\n} from 'lucide-react';\nimport {\n type FC,\n type ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport { cn } from '../lib/utils';\nimport { Button } from '../ui/button';\nimport {\n type StatewireInterruptPredicate,\n useStatewireApprovalRequest,\n} from './use-statewire-approval-request';\n\n/**\n * Human-in-the-loop approval card for the statewire transport — the one\n * approval UI every Athena chat surface mounts (the SDK's `AthenaChat`, the\n * Chrome side panel, the Office task pane, Olympus).\n *\n * When the deep-agent runtime hits an approval-gated tool call, the backend\n * parks the run on an `interrupt` input request. This card reads that request\n * through `useStatewireApprovalRequest` and answers it with\n * `{ type: 'run/input', requestId, response: { type: 'resume', value } }` —\n * the exact command the backend's resume path consumes. Without an answer the\n * run stays parked forever, so this card must be mounted on every statewire\n * chat surface. On legacy (non-statewire) threads it renders nothing.\n *\n * A gated call raised inside a delegated subagent parks only its lane: the\n * host mirrors that interrupt while the sibling lanes keep streaming, before\n * any input request exists. The card renders the mirrored payload the same\n * way and answers it with `run/lane-input` keyed by the lane's langgraph\n * interrupt id (`laneInterruptIds`); the host queues the decision and applies\n * it the moment the run parks, so the approver never waits on the siblings.\n * Once parked, a payload whose calls all carry interrupt ids resumes as a\n * `lane_decisions` map too — one positional `run/input` can only answer the\n * first of several parked lanes.\n *\n * Recovery of a queued decision the host never applies is ordered host\n * first: the host's answerer resumes the park or drops (and flushes) the\n * queue within `QUEUED_LANE_ANSWER_BUDGET_SECONDS` of the park, and this\n * card's grace (`HITL_PENDING_RESUME_GRACE_MS`, longer than that budget)\n * fires only when the host stayed silent. When it fires, the card that sent\n * the queued decision asks the host to replay it (`run/lane-replay` — the\n * host resumes as the approver, under the meta it recorded, and refuses any\n * other caller); every other card, and a replay the host refuses, hands the\n * decision back to a person by unlocking the actions. A queued decision is\n * never re-sent as a `run/input` of the viewing client's own: that would run\n * the approver's decision under the viewer's identity and configuration.\n *\n * Host-specific review UIs (subagent cards, email summaries) slot in through\n * `renderDetails`; a host composer that must also OWN the decision (an\n * editable email whose edits have to reach the backend) takes over the\n * details and the decision row through `renderEditor`; requests another\n * surface answers (typed interrupt families, per-lane subagent approvals)\n * stand down through `isHandledElsewhere`; viewers without edit rights see\n * `readOnlyNotice`.\n */\n\n/** What the card reads in place of the host's queued lanes once it has given up on them. */\nconst NO_HOST_QUEUED_LANE_DECISIONS: Readonly<Record<string, unknown>> = Object.freeze({});\n\n/** The pending request a host slot renders or judges. */\nexport interface StatewireApprovalRequestContext {\n /** The parked input request — `undefined` while the run still streams (a live lane approval). */\n request: StatewireThread.CustomInputRequest | undefined;\n /** The payload narrowed to the Athena HITL approval shape, or `null` for a generic park. */\n hitl: HitlApprovalPayload | null;\n /** Every gated call the approval covers, in decision order (empty for non-HITL parks). */\n actionRequests: PendingActionRequest[];\n}\n\n/** The request a host editor renders, plus the decisions it may send for it. */\nexport interface StatewireApprovalEditorContext extends StatewireApprovalRequestContext {\n /** A decision is in flight (or a live lane already answered); the editor must not send another. */\n locked: boolean;\n /** Whether \"approve all for this session\" may be offered for this batch. */\n sessionApprovalOffered: boolean;\n approve: () => void;\n approveForSession: () => void;\n reject: (reason?: string) => void;\n /**\n * Answer each gated call on its own — the only way an EDITED call reaches\n * the backend. `decision` is asked once per call, in order, so the answer\n * always names exactly the parked calls (a partial map is refused\n * server-side and nothing runs). `null` when the batch cannot take\n * per-action answers: a call without a `tool_call_id`, or a live / multi-\n * lane approval, which resumes per lane and carries no edit.\n */\n decideEach: ((decision: (request: PendingActionRequest) => HitlActionDecision) => void) | null;\n}\n\nexport interface StatewireApprovalCardProps {\n /** Surface-styling overrides merged onto the default classes. */\n classNames?: {\n /** The approval card container. */\n root?: string;\n /** The \"approval was cancelled\" notice container. */\n abandoned?: string;\n /** The disclosed details block. */\n details?: string;\n /** The decision row. */\n actions?: string;\n };\n /** Copy shown when an interrupt payload carries no message. */\n fallbackMessage?: string;\n /**\n * Which pending interrupt this card answers. Defaults to\n * `isApprovalCardInterrupt` (client-tool parks excluded — keep it that way\n * unless the surface handles those itself).\n */\n interruptPredicate?: StatewireInterruptPredicate;\n /**\n * How long a clicked decision keeps the buttons locked when no resume run\n * ever starts (a `run/input` the host rejected before any run began).\n * Defaults to `HITL_PENDING_RESUME_GRACE_MS`.\n */\n pendingResumeGraceMs?: number;\n /**\n * Return true when another surface answers this request (a typed Athena\n * interrupt family, per-lane subagent approval cards). The card renders\n * nothing for it — two surfaces must never answer the same input request.\n */\n isHandledElsewhere?: (context: StatewireApprovalRequestContext) => boolean;\n /**\n * Replace the default argument disclosure (one JSON block per gated call)\n * with a host-specific review UI. Rendered inside the collapsible details\n * region; the decision buttons stay with the card.\n */\n renderDetails?: (context: StatewireApprovalRequestContext) => ReactNode;\n /**\n * Replace BOTH the details region and the decision row with a host\n * composer that decides the request itself through the context's\n * callbacks (an editable email whose edits must be what runs). Return\n * `null`/`undefined` to keep the default card for this request. Not\n * consulted for read-only viewers or non-HITL parks.\n */\n renderEditor?: (context: StatewireApprovalEditorContext) => ReactNode;\n /**\n * The viewer may see the request (it explains why the run is parked) but\n * cannot decide it — answering steers the shared run. Renders\n * `readOnlyNotice` in place of the buttons.\n */\n readOnly?: boolean;\n readOnlyNotice?: ReactNode;\n /**\n * Copy for a park that is not a HITL approval — a typed Athena interrupt\n * this surface has no card for. The only honest action is to decline it\n * (`{ action: 'reject' }`, which every typed raiser treats as a clean user\n * decline); a generic \"Continue\" would be rejected by the raiser's resume\n * schema and surface as a tool error.\n */\n nonHitlNotice?: ReactNode;\n /**\n * The host refused a decision (or the transport lost it). The optimistic\n * fold has rolled back and the request is on screen again with its actions\n * re-enabled; this is where a host tells the user why (a toast, a log line).\n */\n onResumeRejected?: (error: unknown, requestId: string) => void;\n}\n\nexport const StatewireApprovalCard: FC<StatewireApprovalCardProps> = (props) => {\n const extras = useAuiState((s) => s.thread.extras);\n if (!hasStatewireThreadExtras(extras)) return null;\n return <StatewireApprovalCardInner {...props} />;\n};\n\nconst StatewireApprovalCardInner: FC<StatewireApprovalCardProps> = ({\n classNames,\n fallbackMessage,\n interruptPredicate,\n pendingResumeGraceMs,\n isHandledElsewhere,\n renderDetails,\n renderEditor,\n readOnly = false,\n readOnlyNotice = 'Waiting for someone with edit access to respond.',\n nonHitlNotice = 'This request needs an answer this surface cannot collect.',\n onResumeRejected,\n}) => {\n const {\n request,\n requestId,\n hitl: parkedHitl,\n actionRequests: parkedActionRequests,\n liveHitl,\n liveActionRequests,\n queuedLaneDecisions,\n abandonedInterrupt,\n pending,\n resume,\n sendLaneDecisions,\n replayQueuedLanes,\n } = useStatewireApprovalRequest<StatewireThread.InputResponse>({\n interruptPredicate,\n pendingResumeGraceMs,\n onResumeRejected,\n });\n const [dismissedId, setDismissedId] = useState<string>();\n // The card shares the chat column with the thread. A lone gated call is\n // small enough to show inline, but a fan-out of a dozen calls would swallow\n // the conversation, so a batch starts collapsed until someone opens it.\n const [detailsOverride, setDetailsOverride] = useState<boolean>();\n // The live lanes a decision was sent for (joined ids); the buttons stay\n // locked until the host echoes them in `queuedLaneDecisions` or refuses.\n const [liveSentKey, setLiveSentKey] = useState<string>();\n // The parked request whose host-queued lanes outlived the grace window\n // (see `queuedLanesAwaitingHost` below): its queue is read as stale.\n const [staleQueuedRequestId, setStaleQueuedRequestId] = useState<string>();\n // The parked request whose queued lanes this card has asked the host to\n // replay (`run/lane-replay` in flight): locked until the host answers.\n const [replayingRequestId, setReplayingRequestId] = useState<string>();\n // The lanes THIS card decided live (its own `run/lane-input` the host\n // accepted). Only a queue this card recorded is replayed from here — the\n // host refuses any other caller, and a second editor's card must not even\n // ask. Pruned to the lanes on screen whenever a different view arrives, so\n // the record survives the live → parked transition of the same lanes (and\n // a fold that briefly shows nothing) and dies with a new request.\n const sentLaneIdsRef = useRef<Set<string>>(new Set());\n // The parked request already replayed once from here: a request is\n // replayed at most once, then the grace hands the decision back.\n const replayedRequestIdRef = useRef<string>(undefined);\n\n const isLive = request === undefined && liveHitl !== null;\n const hitl = isLive ? liveHitl : parkedHitl;\n const actionRequests = isLive ? liveActionRequests : parkedActionRequests;\n const laneIds = laneInterruptIds(actionRequests);\n // The host answers the lanes it queued from its own settle path, within its\n // published budget of the park (`QUEUED_LANE_ANSWER_BUDGET_SECONDS`). A\n // parked request whose queued lanes are still unanswered past the grace\n // window has a silent answerer, and either this card asked the host to\n // replay them and was refused, or the queue is not this card's to replay —\n // so the queue no longer counts as a decision here: the actions re-enable\n // and the next click answers the parked request itself (a resume the host\n // accepts, and whose dispatch drops the stale queue).\n const hostQueuedLaneDecisions =\n requestId !== undefined && staleQueuedRequestId === requestId\n ? NO_HOST_QUEUED_LANE_DECISIONS\n : queuedLaneDecisions;\n const undecidedLaneIds =\n laneIds?.filter((id) => !(id in hostQueuedLaneDecisions)) ?? null;\n // A lone parked lane (or an unstamped batch) answers with one positional\n // `run/input`; that is the only resume the backend binds per-action answers\n // to, so it is the only place an edited call can go.\n const positional = laneIds === null || (!isLive && laneIds.length <= 1);\n const liveKey = isLive ? (laneIds?.join('|') ?? null) : null;\n const viewKey = requestId ?? liveKey ?? undefined;\n\n // A fresh approval gets the default for ITS batch size, not the disclosure\n // the previous one was left in. Reset during render via React's \"previous\n // value\" pattern.\n const prevViewKeyRef = useRef(viewKey);\n if (prevViewKeyRef.current !== viewKey) {\n prevViewKeyRef.current = viewKey;\n if (detailsOverride !== undefined) setDetailsOverride(undefined);\n if (liveSentKey !== undefined) setLiveSentKey(undefined);\n if (laneIds !== null) {\n for (const id of sentLaneIdsRef.current) {\n if (!laneIds.includes(id)) sentLaneIdsRef.current.delete(id);\n }\n }\n }\n // The per-request marks (stale queue, replayed once, replay in flight)\n // outlive the optimistic fold of a click's own `run/input`: the request\n // leaves and comes back under the SAME id when the host refuses it, and a\n // card that forgot its stale mark then would relock over the standing\n // queue for another grace, and could replay it a second time. They are\n // forgotten only for a genuinely different parked request.\n if (\n requestId !== undefined &&\n staleQueuedRequestId !== undefined &&\n requestId !== staleQueuedRequestId\n ) {\n setStaleQueuedRequestId(undefined);\n }\n if (\n requestId !== undefined &&\n replayingRequestId !== undefined &&\n requestId !== replayingRequestId\n ) {\n setReplayingRequestId(undefined);\n }\n if (\n requestId !== undefined &&\n replayedRequestIdRef.current !== undefined &&\n requestId !== replayedRequestIdRef.current\n ) {\n replayedRequestIdRef.current = undefined;\n }\n\n const context: StatewireApprovalRequestContext = { request, hitl, actionRequests };\n const handledElsewhere = isHandledElsewhere?.(context) ?? false;\n const replaying = replayingRequestId !== undefined && replayingRequestId === requestId;\n\n // Parked, every lane the request covers already queued on the host, no\n // resume of our own in flight, and this card is the one that would decide\n // (not a viewer, not a request another surface owns): the host's answerer\n // is expected to resume the run within its budget. Start the clock on it,\n // longer than that budget. When it runs out the host has stayed silent: if\n // this card queued those lanes itself, it asks the host to replay them as\n // their approver (`run/lane-replay`); otherwise — and when the host refuses\n // the replay — the queue no longer counts as a decision here and the actions\n // re-enable, so a person decides. Never a `run/input` of this client's own\n // carrying someone else's recorded decision.\n const queuedLanesAwaitingHost =\n !isLive &&\n !readOnly &&\n !handledElsewhere &&\n requestId !== undefined &&\n undecidedLaneIds !== null &&\n undecidedLaneIds.length === 0 &&\n !pending &&\n !replaying;\n const queuedLaneGraceMs = pendingResumeGraceMs ?? HITL_PENDING_RESUME_GRACE_MS;\n const laneKey = laneIds?.join('|');\n const onResumeRejectedRef = useRef(onResumeRejected);\n onResumeRejectedRef.current = onResumeRejected;\n useEffect(() => {\n if (!queuedLanesAwaitingHost || requestId === undefined || laneKey === undefined) {\n return undefined;\n }\n const timer = setTimeout(() => {\n const ownQueue =\n laneKey.split('|').every((id) => sentLaneIdsRef.current.has(id)) &&\n replayedRequestIdRef.current !== requestId;\n if (!ownQueue) {\n setStaleQueuedRequestId(requestId);\n return;\n }\n replayedRequestIdRef.current = requestId;\n setReplayingRequestId(requestId);\n replayQueuedLanes(requestId).then(\n () => {\n setReplayingRequestId((current) => (current === requestId ? undefined : current));\n },\n (error: unknown) => {\n setReplayingRequestId((current) => (current === requestId ? undefined : current));\n setStaleQueuedRequestId(requestId);\n onResumeRejectedRef.current?.(error, requestId);\n },\n );\n }, queuedLaneGraceMs);\n return () => clearTimeout(timer);\n }, [queuedLanesAwaitingHost, requestId, laneKey, queuedLaneGraceMs, replayQueuedLanes]);\n\n const decide = useCallback(\n (value: Record<string, string>) => {\n // Several parked lanes (or a live one) answer by interrupt id: a\n // positional `run/input` would resume only the first and re-park the\n // rest. A lone parked lane keeps the positional resume every host\n // already speaks.\n if (positional || undecidedLaneIds === null) {\n resume(value);\n return;\n }\n const lanes = fanLaneDecision({ interruptIds: undecidedLaneIds, decision: value });\n if (!isLive) {\n sendLaneDecisions(lanes);\n return;\n }\n const key = undecidedLaneIds.join('|');\n setLiveSentKey(key);\n Promise.resolve(sendLaneDecisions(lanes)).then(\n () => {\n // The host holds these decisions for this card's caller: they are\n // the only queue this card may ask it to replay.\n for (const id of undecidedLaneIds) sentLaneIdsRef.current.add(id);\n },\n (error: unknown) => {\n setLiveSentKey((current) => (current === key ? undefined : current));\n onResumeRejected?.(error, undecidedLaneIds[0] ?? '');\n },\n );\n },\n [positional, undecidedLaneIds, isLive, resume, sendLaneDecisions, onResumeRejected],\n );\n\n const decideEach = useMemo<StatewireApprovalEditorContext['decideEach']>(() => {\n if (actionRequestToolCallIds(actionRequests) === null || !positional) return null;\n return (decision) => {\n const decisions: Record<string, HitlActionDecision> = {};\n for (const request of actionRequests) {\n if (request.toolCallId !== undefined) decisions[request.toolCallId] = decision(request);\n }\n // The edited argument set comes from the wire as JSON; the resume value\n // is that JSON going back.\n resume(hitlResumeActionDecisions(decisions) as StatewireThread.InputResponse);\n };\n }, [actionRequests, positional, resume]);\n\n if (!request && !isLive) {\n if (!abandonedInterrupt || abandonedInterrupt.id === dismissedId) return null;\n return (\n <div\n className={cn(\n 'aui-approval-abandoned flex w-full items-center justify-between gap-3 rounded-xl border border-amber-300 bg-amber-50 px-3 py-2 text-amber-800 text-xs',\n classNames?.abandoned,\n )}\n >\n <p>Approval was cancelled — the run moved on.</p>\n <button\n type=\"button\"\n aria-label=\"Dismiss cancelled approval\"\n onClick={() => setDismissedId(abandonedInterrupt.id)}\n className=\"shrink-0 rounded-md p-1 hover:bg-amber-100\"\n >\n <XIcon className=\"size-3.5\" />\n </button>\n </div>\n );\n }\n\n if (handledElsewhere) return null;\n // A live mirror without lane stamps cannot be answered before the run\n // parks; the parked request renders it then.\n if (isLive && laneIds === null) return null;\n\n const message = readInterruptMessage(request ? request.payload : liveHitl, fallbackMessage);\n const toolName = hitl?.context?.tool_name ?? hitl?.context?.tool_id;\n const pendingCount = hitl?.context?.pending_action_count ?? 1;\n const hasDetails = hitl !== null && actionRequests.length > 0;\n const showDetails = hasDetails && (detailsOverride ?? actionRequests.length <= 1);\n const sessionApprovalOffered = hitl !== null && canApproveForSession(actionRequests);\n const laneDecided =\n undecidedLaneIds !== null &&\n laneIds !== null &&\n (undecidedLaneIds.length === 0 || liveSentKey === undecidedLaneIds.join('|'));\n const locked = pending || laneDecided || replaying;\n\n const editor =\n !readOnly && hitl !== null && renderEditor\n ? renderEditor({\n ...context,\n locked,\n sessionApprovalOffered,\n approve: () => decide(hitlResumeApprove()),\n approveForSession: () => decide(hitlResumeApproveForSession()),\n reject: (reason) => decide(hitlResumeReject(reason)),\n decideEach,\n })\n : undefined;\n\n return (\n <section\n aria-label={hitl ? 'Approval required' : 'Input required'}\n className={cn(\n 'aui-approval-card w-full rounded-2xl border border-amber-300 bg-amber-50 px-4 py-3 text-amber-900',\n classNames?.root,\n )}\n >\n <div className=\"flex items-start gap-3\">\n <TriangleAlertIcon className=\"mt-0.5 size-4 shrink-0\" />\n <div className=\"min-w-0 flex-1\">\n <p className=\"flex items-center gap-2 font-semibold text-sm\">\n <span>{hitl ? 'Approval required' : 'Waiting for input'}</span>\n {hitl && pendingCount > 1 ? (\n <span className=\"shrink-0 font-normal text-amber-800 text-xs\">\n {pendingCount} tool calls\n </span>\n ) : null}\n </p>\n <p className=\"pt-1 text-amber-800 text-sm leading-snug\">\n {hitl && pendingCount === 1 && toolName ? (\n <>\n <span className=\"font-mono\">{toolName}</span> requires approval before it runs.\n </>\n ) : (\n message\n )}\n </p>\n {isLive ? (\n <p className=\"pt-1 text-amber-700 text-xs\" data-testid=\"statewire-approval-live\">\n {laneDecided\n ? 'Decision sent — it applies as soon as this step pauses.'\n : 'Other work keeps running while you decide.'}\n </p>\n ) : null}\n {hasDetails && editor == null ? (\n <button\n type=\"button\"\n onClick={() => setDetailsOverride(!showDetails)}\n aria-expanded={showDetails}\n className=\"mt-2 flex items-center gap-1 rounded px-1 py-0.5 text-amber-800 text-xs transition-colors hover:text-amber-950\"\n >\n <ChevronDownIcon\n className={cn('size-3 transition-transform', showDetails && 'rotate-180')}\n />\n {showDetails ? 'Hide details' : 'Review details'}\n </button>\n ) : null}\n {/* EVERY pending gated call renders, in order — one decision\n authorizes the whole batch, so nothing may hide behind a host's\n richer cards. All read-only: this runtime resumes through\n `hitl_response_to_decisions`, which has no `tool_args` channel —\n an editable composer would let someone rewrite an email and then\n watch the original go out — unless the host takes over the\n decision too (`renderEditor`), which sends the edit per call. */}\n {editor != null ? (\n <div className={cn('mt-3 flex flex-col gap-2', classNames?.details)}>{editor}</div>\n ) : showDetails ? (\n <div className={cn('mt-2 flex flex-col gap-2', classNames?.details)}>\n {renderDetails ? (\n renderDetails(context)\n ) : (\n <DefaultApprovalDetails actionRequests={actionRequests} />\n )}\n </div>\n ) : null}\n </div>\n </div>\n {editor != null ? null : (\n <div className={cn('flex flex-wrap items-center gap-2 pt-3', classNames?.actions)}>\n {readOnly ? (\n <p className=\"text-amber-800 text-sm\" data-testid=\"statewire-approval-read-only\">\n {readOnlyNotice}\n </p>\n ) : hitl ? (\n <>\n <Button\n size=\"sm\"\n disabled={locked}\n onClick={() => decide(hitlResumeApprove())}\n className=\"bg-green-600 text-white hover:bg-green-700\"\n >\n <CheckIcon />\n Approve\n </Button>\n {sessionApprovalOffered ? (\n <Button\n size=\"sm\"\n variant=\"outline\"\n disabled={locked}\n onClick={() => decide(hitlResumeApproveForSession())}\n data-testid=\"statewire-approval-approve-session\"\n >\n <CheckCheckIcon />\n Approve all for this session\n </Button>\n ) : null}\n <Button\n size=\"sm\"\n variant=\"outline\"\n disabled={locked}\n onClick={() => decide(hitlResumeReject())}\n >\n <XIcon />\n Reject\n </Button>\n </>\n ) : (\n <>\n <p className=\"text-amber-800 text-sm\" data-testid=\"statewire-approval-non-hitl\">\n {nonHitlNotice}\n </p>\n <Button\n size=\"sm\"\n variant=\"outline\"\n disabled={pending}\n onClick={() => resume(interruptResumeDecline())}\n >\n <XIcon />\n Decline\n </Button>\n </>\n )}\n </div>\n )}\n </section>\n );\n};\n\n/** One block per gated call: its name (when the batch has several) and its arguments as JSON. */\nconst DefaultApprovalDetails: FC<{ actionRequests: PendingActionRequest[] }> = ({\n actionRequests,\n}) => (\n <>\n {actionRequests.map((action, index) => (\n <div\n key={`${action.name}-${index}`}\n className=\"rounded-lg bg-amber-100 p-2 text-amber-900 text-xs leading-relaxed\"\n >\n {actionRequests.length > 1 ? (\n <p className=\"font-mono font-semibold\">{action.name}</p>\n ) : null}\n {Object.keys(action.args).length > 0 ? (\n <pre className=\"mt-1 max-h-32 overflow-auto whitespace-pre-wrap break-words rounded bg-white/60 p-1.5 font-mono text-[11px]\">\n {JSON.stringify(action.args, null, 2)}\n </pre>\n ) : (\n <p className=\"text-amber-800\">No arguments.</p>\n )}\n </div>\n ))}\n </>\n);\n"],"names":["_a","request"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"StatewireApprovalCard.js","sources":["../../src/chat/StatewireApprovalCard.tsx"],"sourcesContent":["import { useAuiState } from '@assistant-ui/react';\nimport type { StatewireThread } from '@assistant-ui/react-statewire';\nimport {\n actionRequestToolCallIds,\n canApproveForSession,\n fanLaneDecision,\n HITL_PENDING_RESUME_GRACE_MS,\n hasStatewireThreadExtras,\n type HitlActionDecision,\n type HitlApprovalPayload,\n hitlResumeActionDecisions,\n hitlResumeApprove,\n hitlResumeApproveForSession,\n hitlResumeReject,\n interruptResumeDecline,\n laneInterruptIds,\n type PendingActionRequest,\n type PendingApprovalContext,\n readInterruptMessage,\n} from '@athenaintel/agent-runtime';\nimport {\n CheckCheckIcon,\n CheckIcon,\n ChevronDownIcon,\n TriangleAlertIcon,\n XIcon,\n} from 'lucide-react';\nimport {\n type FC,\n type ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport { cn } from '../lib/utils';\nimport { Button } from '../ui/button';\nimport {\n type StatewireInterruptPredicate,\n useStatewireApprovalRequest,\n} from './use-statewire-approval-request';\n\n/**\n * Human-in-the-loop approval card for the statewire transport — the one\n * approval UI every Athena chat surface mounts (the SDK's `AthenaChat`, the\n * Chrome side panel, the Office task pane, Olympus).\n *\n * When the deep-agent runtime hits an approval-gated tool call, the backend\n * parks the run on an `interrupt` input request. This card reads that request\n * through `useStatewireApprovalRequest` and answers it with\n * `{ type: 'run/input', requestId, response: { type: 'resume', value } }` —\n * the exact command the backend's resume path consumes. Without an answer the\n * run stays parked forever, so this card must be mounted on every statewire\n * chat surface. On legacy (non-statewire) threads it renders nothing.\n *\n * A gated call raised inside a delegated subagent parks only its lane: the\n * host mirrors that interrupt while the sibling lanes keep streaming, before\n * any input request exists. The card renders the mirrored payload the same\n * way and answers it with `run/lane-input` keyed by the lane's langgraph\n * interrupt id (`laneInterruptIds`); the host queues the decision and applies\n * it the moment the run parks, so the approver never waits on the siblings.\n * Once parked, a payload whose calls all carry interrupt ids resumes as a\n * `lane_decisions` map too — one positional `run/input` can only answer the\n * first of several parked lanes.\n *\n * Recovery of a queued decision the host never applies is ordered host\n * first: the host's answerer resumes the park or drops (and flushes) the\n * queue within `QUEUED_LANE_ANSWER_BUDGET_SECONDS` of the park, and this\n * card's grace (`HITL_PENDING_RESUME_GRACE_MS`, longer than that budget)\n * fires only when the host stayed silent. When it fires, the card that sent\n * the queued decision asks the host to replay it (`run/lane-replay` — the\n * host resumes as the approver, under the meta it recorded, and refuses any\n * other caller); every other card, and a replay the host refuses, hands the\n * decision back to a person by unlocking the actions. A queued decision is\n * never re-sent as a `run/input` of the viewing client's own: that would run\n * the approver's decision under the viewer's identity and configuration.\n *\n * Host-specific review UIs (subagent cards, email summaries) slot in through\n * `renderDetails`; a host composer that must also OWN the decision (an\n * editable email whose edits have to reach the backend) takes over the\n * details and the decision row through `renderEditor`; requests another\n * surface answers (typed interrupt families, per-lane subagent approvals)\n * stand down through `isHandledElsewhere`; viewers without edit rights see\n * `readOnlyNotice`.\n */\n\n/** What the card reads in place of the host's queued lanes once it has given up on them. */\nconst NO_HOST_QUEUED_LANE_DECISIONS: Readonly<Record<string, unknown>> = Object.freeze({});\n\n/** The pending request a host slot renders or judges. */\nexport interface StatewireApprovalRequestContext {\n /** The parked input request — `undefined` while the run still streams (a live lane approval). */\n request: StatewireThread.CustomInputRequest | undefined;\n /** The payload narrowed to the Athena HITL approval shape, or `null` for a generic park. */\n hitl: HitlApprovalPayload | null;\n /** Every gated call the approval covers, in decision order (empty for non-HITL parks). */\n actionRequests: PendingActionRequest[];\n}\n\n/** The request a host editor renders, plus the decisions it may send for it. */\nexport interface StatewireApprovalEditorContext extends StatewireApprovalRequestContext {\n /** A decision is in flight (or a live lane already answered); the editor must not send another. */\n locked: boolean;\n /** Whether \"approve all for this session\" may be offered for this batch. */\n sessionApprovalOffered: boolean;\n approve: () => void;\n approveForSession: () => void;\n reject: (reason?: string) => void;\n /**\n * Answer each gated call on its own — the only way an EDITED call reaches\n * the backend. `decision` is asked once per call, in order, so the answer\n * always names exactly the parked calls (a partial map is refused\n * server-side and nothing runs). `null` when the batch cannot take\n * per-action answers: a call without a `tool_call_id`, or a live / multi-\n * lane approval, which resumes per lane and carries no edit.\n */\n decideEach: ((decision: (request: PendingActionRequest) => HitlActionDecision) => void) | null;\n}\n\nexport interface StatewireApprovalCardProps {\n /** Surface-styling overrides merged onto the default classes. */\n classNames?: {\n /** The approval card container. */\n root?: string;\n /** The \"approval was cancelled\" notice container. */\n abandoned?: string;\n /** The disclosed details block. */\n details?: string;\n /** The decision row. */\n actions?: string;\n };\n /** Copy shown when an interrupt payload carries no message. */\n fallbackMessage?: string;\n /**\n * Which pending interrupt this card answers. Defaults to\n * `isApprovalCardInterrupt` (client-tool parks excluded — keep it that way\n * unless the surface handles those itself).\n */\n interruptPredicate?: StatewireInterruptPredicate;\n /**\n * How long a clicked decision keeps the buttons locked when no resume run\n * ever starts (a `run/input` the host rejected before any run began).\n * Defaults to `HITL_PENDING_RESUME_GRACE_MS`.\n */\n pendingResumeGraceMs?: number;\n /**\n * Return true when another surface answers this request (a typed Athena\n * interrupt family, per-lane subagent approval cards). The card renders\n * nothing for it — two surfaces must never answer the same input request.\n */\n isHandledElsewhere?: (context: StatewireApprovalRequestContext) => boolean;\n /**\n * Replace the default argument disclosure (one JSON block per gated call)\n * with a host-specific review UI. Rendered inside the collapsible details\n * region; the decision buttons stay with the card.\n */\n renderDetails?: (context: StatewireApprovalRequestContext) => ReactNode;\n /**\n * Replace BOTH the details region and the decision row with a host\n * composer that decides the request itself through the context's\n * callbacks (an editable email whose edits must be what runs). Return\n * `null`/`undefined` to keep the default card for this request. Not\n * consulted for read-only viewers or non-HITL parks.\n */\n renderEditor?: (context: StatewireApprovalEditorContext) => ReactNode;\n /**\n * The viewer may see the request (it explains why the run is parked) but\n * cannot decide it — answering steers the shared run. Renders\n * `readOnlyNotice` in place of the buttons.\n */\n readOnly?: boolean;\n readOnlyNotice?: ReactNode;\n /**\n * Copy for a park that is not a HITL approval — a typed Athena interrupt\n * this surface has no card for. The only honest action is to decline it\n * (`{ action: 'reject' }`, which every typed raiser treats as a clean user\n * decline); a generic \"Continue\" would be rejected by the raiser's resume\n * schema and surface as a tool error.\n */\n nonHitlNotice?: ReactNode;\n /**\n * The host refused a decision (or the transport lost it). The optimistic\n * fold has rolled back and the request is on screen again with its actions\n * re-enabled; this is where a host tells the user why (a toast, a log line).\n */\n onResumeRejected?: (error: unknown, requestId: string) => void;\n}\n\nexport const StatewireApprovalCard: FC<StatewireApprovalCardProps> = (props) => {\n const extras = useAuiState((s) => s.thread.extras);\n if (!hasStatewireThreadExtras(extras)) return null;\n return <StatewireApprovalCardInner {...props} />;\n};\n\nconst StatewireApprovalCardInner: FC<StatewireApprovalCardProps> = ({\n classNames,\n fallbackMessage,\n interruptPredicate,\n pendingResumeGraceMs,\n isHandledElsewhere,\n renderDetails,\n renderEditor,\n readOnly = false,\n readOnlyNotice = 'Waiting for someone with edit access to respond.',\n nonHitlNotice = 'This request needs an answer this surface cannot collect.',\n onResumeRejected,\n}) => {\n const {\n request,\n requestId,\n hitl: parkedHitl,\n actionRequests: parkedActionRequests,\n liveHitl,\n liveActionRequests,\n queuedLaneDecisions,\n abandonedInterrupt,\n pending,\n resume,\n sendLaneDecisions,\n replayQueuedLanes,\n } = useStatewireApprovalRequest<StatewireThread.InputResponse>({\n interruptPredicate,\n pendingResumeGraceMs,\n onResumeRejected,\n });\n const [dismissedId, setDismissedId] = useState<string>();\n // The card shares the chat column with the thread. A lone gated call is\n // small enough to show inline, but a fan-out of a dozen calls would swallow\n // the conversation, so a batch starts collapsed until someone opens it.\n const [detailsOverride, setDetailsOverride] = useState<boolean>();\n // The live lanes a decision was sent for (joined ids); the buttons stay\n // locked until the host echoes them in `queuedLaneDecisions` or refuses.\n const [liveSentKey, setLiveSentKey] = useState<string>();\n // The parked request whose host-queued lanes outlived the grace window\n // (see `queuedLanesAwaitingHost` below): its queue is read as stale.\n const [staleQueuedRequestId, setStaleQueuedRequestId] = useState<string>();\n // The parked request whose queued lanes this card has asked the host to\n // replay (`run/lane-replay` in flight): locked until the host answers.\n const [replayingRequestId, setReplayingRequestId] = useState<string>();\n // The lanes THIS card decided live (its own `run/lane-input` the host\n // accepted). Only a queue this card recorded is replayed from here — the\n // host refuses any other caller, and a second editor's card must not even\n // ask. Pruned to the lanes on screen whenever a different view arrives, so\n // the record survives the live → parked transition of the same lanes (and\n // a fold that briefly shows nothing) and dies with a new request.\n const sentLaneIdsRef = useRef<Set<string>>(new Set());\n // The parked request already replayed once from here: a request is\n // replayed at most once, then the grace hands the decision back.\n const replayedRequestIdRef = useRef<string>(undefined);\n\n const isLive = request === undefined && liveHitl !== null;\n const hitl = isLive ? liveHitl : parkedHitl;\n const actionRequests = isLive ? liveActionRequests : parkedActionRequests;\n const laneIds = laneInterruptIds(actionRequests);\n // The host answers the lanes it queued from its own settle path, within its\n // published budget of the park (`QUEUED_LANE_ANSWER_BUDGET_SECONDS`). A\n // parked request whose queued lanes are still unanswered past the grace\n // window has a silent answerer, and either this card asked the host to\n // replay them and was refused, or the queue is not this card's to replay —\n // so the queue no longer counts as a decision here: the actions re-enable\n // and the next click answers the parked request itself (a resume the host\n // accepts, and whose dispatch drops the stale queue).\n const hostQueuedLaneDecisions =\n requestId !== undefined && staleQueuedRequestId === requestId\n ? NO_HOST_QUEUED_LANE_DECISIONS\n : queuedLaneDecisions;\n const undecidedLaneIds =\n laneIds?.filter((id) => !(id in hostQueuedLaneDecisions)) ?? null;\n // A lone parked lane (or an unstamped batch) answers with one positional\n // `run/input`; that is the only resume the backend binds per-action answers\n // to, so it is the only place an edited call can go.\n const positional = laneIds === null || (!isLive && laneIds.length <= 1);\n const liveKey = isLive ? (laneIds?.join('|') ?? null) : null;\n const viewKey = requestId ?? liveKey ?? undefined;\n\n // A fresh approval gets the default for ITS batch size, not the disclosure\n // the previous one was left in. Reset during render via React's \"previous\n // value\" pattern.\n const prevViewKeyRef = useRef(viewKey);\n if (prevViewKeyRef.current !== viewKey) {\n prevViewKeyRef.current = viewKey;\n if (detailsOverride !== undefined) setDetailsOverride(undefined);\n if (liveSentKey !== undefined) setLiveSentKey(undefined);\n if (laneIds !== null) {\n for (const id of sentLaneIdsRef.current) {\n if (!laneIds.includes(id)) sentLaneIdsRef.current.delete(id);\n }\n }\n }\n // The per-request marks (stale queue, replayed once, replay in flight)\n // outlive the optimistic fold of a click's own `run/input`: the request\n // leaves and comes back under the SAME id when the host refuses it, and a\n // card that forgot its stale mark then would relock over the standing\n // queue for another grace, and could replay it a second time. They are\n // forgotten only for a genuinely different parked request.\n if (\n requestId !== undefined &&\n staleQueuedRequestId !== undefined &&\n requestId !== staleQueuedRequestId\n ) {\n setStaleQueuedRequestId(undefined);\n }\n if (\n requestId !== undefined &&\n replayingRequestId !== undefined &&\n requestId !== replayingRequestId\n ) {\n setReplayingRequestId(undefined);\n }\n if (\n requestId !== undefined &&\n replayedRequestIdRef.current !== undefined &&\n requestId !== replayedRequestIdRef.current\n ) {\n replayedRequestIdRef.current = undefined;\n }\n\n const context: StatewireApprovalRequestContext = { request, hitl, actionRequests };\n const handledElsewhere = isHandledElsewhere?.(context) ?? false;\n const replaying = replayingRequestId !== undefined && replayingRequestId === requestId;\n\n // Parked, every lane the request covers already queued on the host, no\n // resume of our own in flight, and this card is the one that would decide\n // (not a viewer, not a request another surface owns): the host's answerer\n // is expected to resume the run within its budget. Start the clock on it,\n // longer than that budget. When it runs out the host has stayed silent: if\n // this card queued those lanes itself, it asks the host to replay them as\n // their approver (`run/lane-replay`); otherwise — and when the host refuses\n // the replay — the queue no longer counts as a decision here and the actions\n // re-enable, so a person decides. Never a `run/input` of this client's own\n // carrying someone else's recorded decision.\n const queuedLanesAwaitingHost =\n !isLive &&\n !readOnly &&\n !handledElsewhere &&\n requestId !== undefined &&\n undecidedLaneIds !== null &&\n undecidedLaneIds.length === 0 &&\n !pending &&\n !replaying;\n const queuedLaneGraceMs = pendingResumeGraceMs ?? HITL_PENDING_RESUME_GRACE_MS;\n const laneKey = laneIds?.join('|');\n const onResumeRejectedRef = useRef(onResumeRejected);\n onResumeRejectedRef.current = onResumeRejected;\n useEffect(() => {\n if (!queuedLanesAwaitingHost || requestId === undefined || laneKey === undefined) {\n return undefined;\n }\n const timer = setTimeout(() => {\n const ownQueue =\n laneKey.split('|').every((id) => sentLaneIdsRef.current.has(id)) &&\n replayedRequestIdRef.current !== requestId;\n if (!ownQueue) {\n setStaleQueuedRequestId(requestId);\n return;\n }\n replayedRequestIdRef.current = requestId;\n setReplayingRequestId(requestId);\n replayQueuedLanes(requestId).then(\n () => {\n setReplayingRequestId((current) => (current === requestId ? undefined : current));\n },\n (error: unknown) => {\n setReplayingRequestId((current) => (current === requestId ? undefined : current));\n setStaleQueuedRequestId(requestId);\n onResumeRejectedRef.current?.(error, requestId);\n },\n );\n }, queuedLaneGraceMs);\n return () => clearTimeout(timer);\n }, [queuedLanesAwaitingHost, requestId, laneKey, queuedLaneGraceMs, replayQueuedLanes]);\n\n const decide = useCallback(\n (value: Record<string, string>) => {\n // Several parked lanes (or a live one) answer by interrupt id: a\n // positional `run/input` would resume only the first and re-park the\n // rest. A lone parked lane keeps the positional resume every host\n // already speaks.\n if (positional || undecidedLaneIds === null) {\n resume(value);\n return;\n }\n const lanes = fanLaneDecision({ interruptIds: undecidedLaneIds, decision: value });\n if (!isLive) {\n sendLaneDecisions(lanes);\n return;\n }\n const key = undecidedLaneIds.join('|');\n setLiveSentKey(key);\n Promise.resolve(sendLaneDecisions(lanes)).then(\n () => {\n // The host holds these decisions for this card's caller: they are\n // the only queue this card may ask it to replay.\n for (const id of undecidedLaneIds) sentLaneIdsRef.current.add(id);\n },\n (error: unknown) => {\n setLiveSentKey((current) => (current === key ? undefined : current));\n onResumeRejected?.(error, undecidedLaneIds[0] ?? '');\n },\n );\n },\n [positional, undecidedLaneIds, isLive, resume, sendLaneDecisions, onResumeRejected],\n );\n\n const decideEach = useMemo<StatewireApprovalEditorContext['decideEach']>(() => {\n if (actionRequestToolCallIds(actionRequests) === null || !positional) return null;\n return (decision) => {\n const decisions: Record<string, HitlActionDecision> = {};\n for (const request of actionRequests) {\n if (request.toolCallId !== undefined) decisions[request.toolCallId] = decision(request);\n }\n // The edited argument set comes from the wire as JSON; the resume value\n // is that JSON going back.\n resume(hitlResumeActionDecisions(decisions) as StatewireThread.InputResponse);\n };\n }, [actionRequests, positional, resume]);\n\n if (!request && !isLive) {\n if (!abandonedInterrupt || abandonedInterrupt.id === dismissedId) return null;\n return (\n <div\n className={cn(\n 'aui-approval-abandoned flex w-full items-center justify-between gap-3 rounded-xl border border-amber-300 bg-amber-50 px-3 py-2 text-amber-800 text-xs',\n classNames?.abandoned,\n )}\n >\n <p>Approval was cancelled — the run moved on.</p>\n <button\n type=\"button\"\n aria-label=\"Dismiss cancelled approval\"\n onClick={() => setDismissedId(abandonedInterrupt.id)}\n className=\"shrink-0 rounded-md p-1 hover:bg-amber-100\"\n >\n <XIcon className=\"size-3.5\" />\n </button>\n </div>\n );\n }\n\n if (handledElsewhere) return null;\n // A live mirror without lane stamps cannot be answered before the run\n // parks; the parked request renders it then.\n if (isLive && laneIds === null) return null;\n\n const message = readInterruptMessage(request ? request.payload : liveHitl, fallbackMessage);\n const toolName = hitl?.context?.tool_name ?? hitl?.context?.tool_id;\n const pendingCount = hitl?.context?.pending_action_count ?? 1;\n const hasDetails = hitl !== null && actionRequests.length > 0;\n const showDetails = hasDetails && (detailsOverride ?? actionRequests.length <= 1);\n const sessionApprovalOffered = hitl !== null && canApproveForSession(actionRequests);\n const laneDecided =\n undecidedLaneIds !== null &&\n laneIds !== null &&\n (undecidedLaneIds.length === 0 || liveSentKey === undecidedLaneIds.join('|'));\n const locked = pending || laneDecided || replaying;\n\n const editor =\n !readOnly && hitl !== null && renderEditor\n ? renderEditor({\n ...context,\n locked,\n sessionApprovalOffered,\n approve: () => decide(hitlResumeApprove()),\n approveForSession: () => decide(hitlResumeApproveForSession()),\n reject: (reason) => decide(hitlResumeReject(reason)),\n decideEach,\n })\n : undefined;\n\n return (\n <section\n aria-label={hitl ? 'Approval required' : 'Input required'}\n className={cn(\n 'aui-approval-card w-full rounded-2xl border border-amber-300 bg-amber-50 px-4 py-3 text-amber-900',\n classNames?.root,\n )}\n >\n <div className=\"flex items-start gap-3\">\n <TriangleAlertIcon className=\"mt-0.5 size-4 shrink-0\" />\n <div className=\"min-w-0 flex-1\">\n <p className=\"flex items-center gap-2 font-semibold text-sm\">\n <span>{hitl ? 'Approval required' : 'Waiting for input'}</span>\n {hitl && pendingCount > 1 ? (\n <span className=\"shrink-0 font-normal text-amber-800 text-xs\">\n {pendingCount} tool calls\n </span>\n ) : null}\n </p>\n <p className=\"pt-1 text-amber-800 text-sm leading-snug\">\n {hitl && pendingCount === 1 && toolName ? (\n <>\n <span className=\"font-mono\">{toolName}</span> requires approval before it runs.\n </>\n ) : (\n message\n )}\n </p>\n {isLive ? (\n <p className=\"pt-1 text-amber-700 text-xs\" data-testid=\"statewire-approval-live\">\n {laneDecided\n ? 'Decision sent — it applies as soon as this step pauses.'\n : 'Other work keeps running while you decide.'}\n </p>\n ) : null}\n {hasDetails && editor == null ? (\n <button\n type=\"button\"\n onClick={() => setDetailsOverride(!showDetails)}\n aria-expanded={showDetails}\n className=\"mt-2 flex items-center gap-1 rounded px-1 py-0.5 text-amber-800 text-xs transition-colors hover:text-amber-950\"\n >\n <ChevronDownIcon\n className={cn('size-3 transition-transform', showDetails && 'rotate-180')}\n />\n {showDetails ? 'Hide details' : 'Review details'}\n </button>\n ) : null}\n {/* EVERY pending gated call renders, in order — one decision\n authorizes the whole batch, so nothing may hide behind a host's\n richer cards. All read-only: this runtime resumes through\n `hitl_response_to_decisions`, which has no `tool_args` channel —\n an editable composer would let someone rewrite an email and then\n watch the original go out — unless the host takes over the\n decision too (`renderEditor`), which sends the edit per call. */}\n {editor != null ? (\n <div className={cn('mt-3 flex flex-col gap-2', classNames?.details)}>{editor}</div>\n ) : showDetails ? (\n <div className={cn('mt-2 flex flex-col gap-2', classNames?.details)}>\n {renderDetails ? (\n renderDetails(context)\n ) : (\n <DefaultApprovalDetails actionRequests={actionRequests} />\n )}\n </div>\n ) : null}\n </div>\n </div>\n {editor != null ? null : (\n <div className={cn('flex flex-wrap items-center gap-2 pt-3', classNames?.actions)}>\n {readOnly ? (\n <p className=\"text-amber-800 text-sm\" data-testid=\"statewire-approval-read-only\">\n {readOnlyNotice}\n </p>\n ) : hitl ? (\n <>\n <Button\n size=\"sm\"\n disabled={locked}\n onClick={() => decide(hitlResumeApprove())}\n className=\"bg-green-600 text-white hover:bg-green-700\"\n >\n <CheckIcon />\n Approve\n </Button>\n {sessionApprovalOffered ? (\n <Button\n size=\"sm\"\n variant=\"outline\"\n disabled={locked}\n onClick={() => decide(hitlResumeApproveForSession())}\n data-testid=\"statewire-approval-approve-session\"\n >\n <CheckCheckIcon />\n Approve all for this session\n </Button>\n ) : null}\n <Button\n size=\"sm\"\n variant=\"outline\"\n disabled={locked}\n onClick={() => decide(hitlResumeReject())}\n >\n <XIcon />\n Reject\n </Button>\n </>\n ) : (\n <>\n <p className=\"text-amber-800 text-sm\" data-testid=\"statewire-approval-non-hitl\">\n {nonHitlNotice}\n </p>\n <Button\n size=\"sm\"\n variant=\"outline\"\n disabled={pending}\n onClick={() => resume(interruptResumeDecline())}\n >\n <XIcon />\n Decline\n </Button>\n </>\n )}\n </div>\n )}\n </section>\n );\n};\n\n/**\n * What the backend resolved about a call before asking (where a Slack write\n * goes and as whom, Slack Connect warnings), above its raw arguments.\n */\nconst ApprovalContextFacts: FC<{ context: PendingApprovalContext }> = ({ context }) => (\n <div className=\"mt-1 space-y-0.5\" data-testid=\"statewire-approval-context\">\n {context.facts.map((fact) => (\n <p key={fact.label}>\n <span className=\"font-semibold\">{fact.label}:</span> {fact.value}\n </p>\n ))}\n {context.warnings.map((warning) => (\n <p\n key={warning.message}\n className={warning.severity === 'info' ? undefined : 'font-semibold'}\n >\n {warning.message}\n </p>\n ))}\n </div>\n);\n\n/** One block per gated call: its name (when the batch has several), its resolved facts, and its arguments as JSON. */\nconst DefaultApprovalDetails: FC<{ actionRequests: PendingActionRequest[] }> = ({\n actionRequests,\n}) => (\n <>\n {actionRequests.map((action, index) => (\n <div\n key={`${action.name}-${index}`}\n className=\"rounded-lg bg-amber-100 p-2 text-amber-900 text-xs leading-relaxed\"\n >\n {actionRequests.length > 1 ? (\n <p className=\"font-mono font-semibold\">{action.name}</p>\n ) : null}\n {action.approvalContext ? <ApprovalContextFacts context={action.approvalContext} /> : null}\n {Object.keys(action.args).length > 0 ? (\n <pre className=\"mt-1 max-h-32 overflow-auto whitespace-pre-wrap break-words rounded bg-white/60 p-1.5 font-mono text-[11px]\">\n {JSON.stringify(action.args, null, 2)}\n </pre>\n ) : (\n <p className=\"text-amber-800\">No arguments.</p>\n )}\n </div>\n ))}\n </>\n);\n"],"names":["_a","request"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"message-time.cjs","sources":["../../src/chat/message-time.ts"],"sourcesContent":["/**\n * When a message was created, for the thread's hover timestamps and day\n * separators.\n *\n * The runtime stamps every deep-agent message with `_created_at` (an ISO\n * instant) inside `additional_kwargs.metadata`, the one channel the shared\n * converter exposes as `message.metadata.custom`. That stamp is the only\n * truthful clock: assistant-ui fills `message.createdAt` with `new Date()`
|
|
1
|
+
{"version":3,"file":"message-time.cjs","sources":["../../src/chat/message-time.ts"],"sourcesContent":["/**\n * When a message was created, for the thread's hover timestamps and day\n * separators.\n *\n * The runtime stamps every deep-agent message with `_created_at` (an ISO\n * instant) inside `additional_kwargs.metadata`, the one channel the shared\n * converter exposes as `message.metadata.custom`. That stamp is the only\n * truthful clock: assistant-ui fills `message.createdAt` with `new Date()` when\n * it converts the message — when this client first saw it, not when it was\n * sent (a reload re-stamps the whole history) — so a message without the stamp\n * shows no time rather than a wrong one.\n */\n\nexport const MESSAGE_CREATED_AT_KEY = \"_created_at\";\n\nexport interface MessageTimeFormatOptions {\n /** BCP 47 locale; the browser's by default. Tests pass one for determinism. */\n locale?: string;\n /** IANA time zone; the browser's by default. Tests pass one for determinism. */\n timeZone?: string;\n}\n\n/** The stamp as a `Date`, or `null` when absent or unparseable. */\nexport function parseMessageTime(value: unknown): Date | null {\n if (typeof value !== \"string\" || value.length === 0) return null;\n const time = Date.parse(value);\n return Number.isNaN(time) ? null : new Date(time);\n}\n\n/** The `_created_at` stamp off a message's `metadata.custom`, when it carries a valid one. */\nexport function readMessageCreatedAt(\n custom: Record<string, unknown> | undefined,\n): string | null {\n const value = custom?.[MESSAGE_CREATED_AT_KEY];\n return parseMessageTime(value) ? (value as string) : null;\n}\n\n/** A `YYYY-MM-DD` key for the calendar day `date` falls on in `timeZone`. */\nexport function dayKey(date: Date, timeZone?: string): string {\n // en-CA formats as YYYY-MM-DD, which sorts and compares as plain text.\n return new Intl.DateTimeFormat(\"en-CA\", {\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n ...(timeZone ? { timeZone } : {}),\n }).format(date);\n}\n\n/** Whether two instants fall on the same calendar day. */\nexport function isSameCalendarDay(a: Date, b: Date, timeZone?: string): boolean {\n return dayKey(a, timeZone) === dayKey(b, timeZone);\n}\n\n/**\n * The calendar day before a `YYYY-MM-DD` key. Computed on the calendar, not\n * by subtracting 24 hours from an instant: across a daylight-saving change a\n * local day is 23 or 25 hours long, so an instant a day earlier can land two\n * calendar days back (or on the same day) just after midnight.\n */\nexport function previousDayKey(key: string): string {\n const [year, month, day] = key.split(\"-\").map(Number);\n return dayKey(new Date(Date.UTC(year!, month! - 1, day! - 1)), \"UTC\");\n}\n\n/** The short clock time shown on hover (\"3:42 PM\"). */\nexport function formatMessageTime(\n date: Date,\n { locale, timeZone }: MessageTimeFormatOptions = {},\n): string {\n return new Intl.DateTimeFormat(locale, {\n hour: \"numeric\",\n minute: \"2-digit\",\n ...(timeZone ? { timeZone } : {}),\n }).format(date);\n}\n\n/** The full date and time for a `title` (\"Monday, September 21, 2026 at 3:42 PM\"). */\nexport function formatMessageDateTime(\n date: Date,\n { locale, timeZone }: MessageTimeFormatOptions = {},\n): string {\n return new Intl.DateTimeFormat(locale, {\n dateStyle: \"full\",\n timeStyle: \"short\",\n ...(timeZone ? { timeZone } : {}),\n }).format(date);\n}\n\n/**\n * The day separator's label: \"Today\", \"Yesterday\", else the weekday and date\n * (\"Mon, Sep 21\"), with the year once the day is not in the current year\n * (\"Sep 21, 2025\").\n */\nexport function formatDayLabel(\n date: Date,\n { now = new Date(), locale, timeZone }: MessageTimeFormatOptions & { now?: Date } = {},\n): string {\n const key = dayKey(date, timeZone);\n const todayKey = dayKey(now, timeZone);\n if (key === todayKey) return \"Today\";\n if (key === previousDayKey(todayKey)) return \"Yesterday\";\n const sameYear = key.slice(0, 4) === todayKey.slice(0, 4);\n return new Intl.DateTimeFormat(locale, {\n ...(sameYear ? { weekday: \"short\" } : { year: \"numeric\" }),\n month: \"short\",\n day: \"numeric\",\n ...(timeZone ? { timeZone } : {}),\n }).format(date);\n}\n"],"names":[],"mappings":";;AAaO,MAAM,yBAAyB;AAU/B,SAAS,iBAAiB,OAA6B;AAC5D,MAAI,OAAO,UAAU,YAAY,MAAM,WAAW,EAAG,QAAO;AAC5D,QAAM,OAAO,KAAK,MAAM,KAAK;AAC7B,SAAO,OAAO,MAAM,IAAI,IAAI,OAAO,IAAI,KAAK,IAAI;AAClD;AAGO,SAAS,qBACd,QACe;AACf,QAAM,QAAQ,iCAAS;AACvB,SAAO,iBAAiB,KAAK,IAAK,QAAmB;AACvD;AAGO,SAAS,OAAO,MAAY,UAA2B;AAE5D,SAAO,IAAI,KAAK,eAAe,SAAS;AAAA,IACtC,MAAM;AAAA,IACN,OAAO;AAAA,IACP,KAAK;AAAA,IACL,GAAI,WAAW,EAAE,aAAa,CAAA;AAAA,EAAC,CAChC,EAAE,OAAO,IAAI;AAChB;AAGO,SAAS,kBAAkB,GAAS,GAAS,UAA4B;AAC9E,SAAO,OAAO,GAAG,QAAQ,MAAM,OAAO,GAAG,QAAQ;AACnD;AAQO,SAAS,eAAe,KAAqB;AAClD,QAAM,CAAC,MAAM,OAAO,GAAG,IAAI,IAAI,MAAM,GAAG,EAAE,IAAI,MAAM;AACpD,SAAO,OAAO,IAAI,KAAK,KAAK,IAAI,MAAO,QAAS,GAAG,MAAO,CAAC,CAAC,GAAG,KAAK;AACtE;AAGO,SAAS,kBACd,MACA,EAAE,QAAQ,SAAA,IAAuC,CAAA,GACzC;AACR,SAAO,IAAI,KAAK,eAAe,QAAQ;AAAA,IACrC,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,GAAI,WAAW,EAAE,aAAa,CAAA;AAAA,EAAC,CAChC,EAAE,OAAO,IAAI;AAChB;AAGO,SAAS,sBACd,MACA,EAAE,QAAQ,SAAA,IAAuC,CAAA,GACzC;AACR,SAAO,IAAI,KAAK,eAAe,QAAQ;AAAA,IACrC,WAAW;AAAA,IACX,WAAW;AAAA,IACX,GAAI,WAAW,EAAE,aAAa,CAAA;AAAA,EAAC,CAChC,EAAE,OAAO,IAAI;AAChB;AAOO,SAAS,eACd,MACA,EAAE,MAAM,oBAAI,KAAA,GAAQ,QAAQ,SAAA,IAAwD,IAC5E;AACR,QAAM,MAAM,OAAO,MAAM,QAAQ;AACjC,QAAM,WAAW,OAAO,KAAK,QAAQ;AACrC,MAAI,QAAQ,SAAU,QAAO;AAC7B,MAAI,QAAQ,eAAe,QAAQ,EAAG,QAAO;AAC7C,QAAM,WAAW,IAAI,MAAM,GAAG,CAAC,MAAM,SAAS,MAAM,GAAG,CAAC;AACxD,SAAO,IAAI,KAAK,eAAe,QAAQ;AAAA,IACrC,GAAI,WAAW,EAAE,SAAS,YAAY,EAAE,MAAM,UAAA;AAAA,IAC9C,OAAO;AAAA,IACP,KAAK;AAAA,IACL,GAAI,WAAW,EAAE,aAAa,CAAA;AAAA,EAAC,CAChC,EAAE,OAAO,IAAI;AAChB;;;;;;;;;;"}
|
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* The runtime stamps every deep-agent message with `_created_at` (an ISO
|
|
6
6
|
* instant) inside `additional_kwargs.metadata`, the one channel the shared
|
|
7
7
|
* converter exposes as `message.metadata.custom`. That stamp is the only
|
|
8
|
-
* truthful clock: assistant-ui fills `message.createdAt` with `new Date()`
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* truthful clock: assistant-ui fills `message.createdAt` with `new Date()` when
|
|
9
|
+
* it converts the message — when this client first saw it, not when it was
|
|
10
|
+
* sent (a reload re-stamps the whole history) — so a message without the stamp
|
|
11
|
+
* shows no time rather than a wrong one.
|
|
11
12
|
*/
|
|
12
13
|
export declare const MESSAGE_CREATED_AT_KEY = "_created_at";
|
|
13
14
|
export interface MessageTimeFormatOptions {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"message-time.js","sources":["../../src/chat/message-time.ts"],"sourcesContent":["/**\n * When a message was created, for the thread's hover timestamps and day\n * separators.\n *\n * The runtime stamps every deep-agent message with `_created_at` (an ISO\n * instant) inside `additional_kwargs.metadata`, the one channel the shared\n * converter exposes as `message.metadata.custom`. That stamp is the only\n * truthful clock: assistant-ui fills `message.createdAt` with `new Date()`
|
|
1
|
+
{"version":3,"file":"message-time.js","sources":["../../src/chat/message-time.ts"],"sourcesContent":["/**\n * When a message was created, for the thread's hover timestamps and day\n * separators.\n *\n * The runtime stamps every deep-agent message with `_created_at` (an ISO\n * instant) inside `additional_kwargs.metadata`, the one channel the shared\n * converter exposes as `message.metadata.custom`. That stamp is the only\n * truthful clock: assistant-ui fills `message.createdAt` with `new Date()` when\n * it converts the message — when this client first saw it, not when it was\n * sent (a reload re-stamps the whole history) — so a message without the stamp\n * shows no time rather than a wrong one.\n */\n\nexport const MESSAGE_CREATED_AT_KEY = \"_created_at\";\n\nexport interface MessageTimeFormatOptions {\n /** BCP 47 locale; the browser's by default. Tests pass one for determinism. */\n locale?: string;\n /** IANA time zone; the browser's by default. Tests pass one for determinism. */\n timeZone?: string;\n}\n\n/** The stamp as a `Date`, or `null` when absent or unparseable. */\nexport function parseMessageTime(value: unknown): Date | null {\n if (typeof value !== \"string\" || value.length === 0) return null;\n const time = Date.parse(value);\n return Number.isNaN(time) ? null : new Date(time);\n}\n\n/** The `_created_at` stamp off a message's `metadata.custom`, when it carries a valid one. */\nexport function readMessageCreatedAt(\n custom: Record<string, unknown> | undefined,\n): string | null {\n const value = custom?.[MESSAGE_CREATED_AT_KEY];\n return parseMessageTime(value) ? (value as string) : null;\n}\n\n/** A `YYYY-MM-DD` key for the calendar day `date` falls on in `timeZone`. */\nexport function dayKey(date: Date, timeZone?: string): string {\n // en-CA formats as YYYY-MM-DD, which sorts and compares as plain text.\n return new Intl.DateTimeFormat(\"en-CA\", {\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n ...(timeZone ? { timeZone } : {}),\n }).format(date);\n}\n\n/** Whether two instants fall on the same calendar day. */\nexport function isSameCalendarDay(a: Date, b: Date, timeZone?: string): boolean {\n return dayKey(a, timeZone) === dayKey(b, timeZone);\n}\n\n/**\n * The calendar day before a `YYYY-MM-DD` key. Computed on the calendar, not\n * by subtracting 24 hours from an instant: across a daylight-saving change a\n * local day is 23 or 25 hours long, so an instant a day earlier can land two\n * calendar days back (or on the same day) just after midnight.\n */\nexport function previousDayKey(key: string): string {\n const [year, month, day] = key.split(\"-\").map(Number);\n return dayKey(new Date(Date.UTC(year!, month! - 1, day! - 1)), \"UTC\");\n}\n\n/** The short clock time shown on hover (\"3:42 PM\"). */\nexport function formatMessageTime(\n date: Date,\n { locale, timeZone }: MessageTimeFormatOptions = {},\n): string {\n return new Intl.DateTimeFormat(locale, {\n hour: \"numeric\",\n minute: \"2-digit\",\n ...(timeZone ? { timeZone } : {}),\n }).format(date);\n}\n\n/** The full date and time for a `title` (\"Monday, September 21, 2026 at 3:42 PM\"). */\nexport function formatMessageDateTime(\n date: Date,\n { locale, timeZone }: MessageTimeFormatOptions = {},\n): string {\n return new Intl.DateTimeFormat(locale, {\n dateStyle: \"full\",\n timeStyle: \"short\",\n ...(timeZone ? { timeZone } : {}),\n }).format(date);\n}\n\n/**\n * The day separator's label: \"Today\", \"Yesterday\", else the weekday and date\n * (\"Mon, Sep 21\"), with the year once the day is not in the current year\n * (\"Sep 21, 2025\").\n */\nexport function formatDayLabel(\n date: Date,\n { now = new Date(), locale, timeZone }: MessageTimeFormatOptions & { now?: Date } = {},\n): string {\n const key = dayKey(date, timeZone);\n const todayKey = dayKey(now, timeZone);\n if (key === todayKey) return \"Today\";\n if (key === previousDayKey(todayKey)) return \"Yesterday\";\n const sameYear = key.slice(0, 4) === todayKey.slice(0, 4);\n return new Intl.DateTimeFormat(locale, {\n ...(sameYear ? { weekday: \"short\" } : { year: \"numeric\" }),\n month: \"short\",\n day: \"numeric\",\n ...(timeZone ? { timeZone } : {}),\n }).format(date);\n}\n"],"names":[],"mappings":"AAaO,MAAM,yBAAyB;AAU/B,SAAS,iBAAiB,OAA6B;AAC5D,MAAI,OAAO,UAAU,YAAY,MAAM,WAAW,EAAG,QAAO;AAC5D,QAAM,OAAO,KAAK,MAAM,KAAK;AAC7B,SAAO,OAAO,MAAM,IAAI,IAAI,OAAO,IAAI,KAAK,IAAI;AAClD;AAGO,SAAS,qBACd,QACe;AACf,QAAM,QAAQ,iCAAS;AACvB,SAAO,iBAAiB,KAAK,IAAK,QAAmB;AACvD;AAGO,SAAS,OAAO,MAAY,UAA2B;AAE5D,SAAO,IAAI,KAAK,eAAe,SAAS;AAAA,IACtC,MAAM;AAAA,IACN,OAAO;AAAA,IACP,KAAK;AAAA,IACL,GAAI,WAAW,EAAE,aAAa,CAAA;AAAA,EAAC,CAChC,EAAE,OAAO,IAAI;AAChB;AAGO,SAAS,kBAAkB,GAAS,GAAS,UAA4B;AAC9E,SAAO,OAAO,GAAG,QAAQ,MAAM,OAAO,GAAG,QAAQ;AACnD;AAQO,SAAS,eAAe,KAAqB;AAClD,QAAM,CAAC,MAAM,OAAO,GAAG,IAAI,IAAI,MAAM,GAAG,EAAE,IAAI,MAAM;AACpD,SAAO,OAAO,IAAI,KAAK,KAAK,IAAI,MAAO,QAAS,GAAG,MAAO,CAAC,CAAC,GAAG,KAAK;AACtE;AAGO,SAAS,kBACd,MACA,EAAE,QAAQ,SAAA,IAAuC,CAAA,GACzC;AACR,SAAO,IAAI,KAAK,eAAe,QAAQ;AAAA,IACrC,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,GAAI,WAAW,EAAE,aAAa,CAAA;AAAA,EAAC,CAChC,EAAE,OAAO,IAAI;AAChB;AAGO,SAAS,sBACd,MACA,EAAE,QAAQ,SAAA,IAAuC,CAAA,GACzC;AACR,SAAO,IAAI,KAAK,eAAe,QAAQ;AAAA,IACrC,WAAW;AAAA,IACX,WAAW;AAAA,IACX,GAAI,WAAW,EAAE,aAAa,CAAA;AAAA,EAAC,CAChC,EAAE,OAAO,IAAI;AAChB;AAOO,SAAS,eACd,MACA,EAAE,MAAM,oBAAI,KAAA,GAAQ,QAAQ,SAAA,IAAwD,IAC5E;AACR,QAAM,MAAM,OAAO,MAAM,QAAQ;AACjC,QAAM,WAAW,OAAO,KAAK,QAAQ;AACrC,MAAI,QAAQ,SAAU,QAAO;AAC7B,MAAI,QAAQ,eAAe,QAAQ,EAAG,QAAO;AAC7C,QAAM,WAAW,IAAI,MAAM,GAAG,CAAC,MAAM,SAAS,MAAM,GAAG,CAAC;AACxD,SAAO,IAAI,KAAK,eAAe,QAAQ;AAAA,IACrC,GAAI,WAAW,EAAE,SAAS,YAAY,EAAE,MAAM,UAAA;AAAA,IAC9C,OAAO;AAAA,IACP,KAAK;AAAA,IACL,GAAI,WAAW,EAAE,aAAa,CAAA;AAAA,EAAC,CAChC,EAAE,OAAO,IAAI;AAChB;"}
|
|
@@ -16,6 +16,17 @@ function useQuote() {
|
|
|
16
16
|
}
|
|
17
17
|
return ctx;
|
|
18
18
|
}
|
|
19
|
+
function isDirectChildFrame(source) {
|
|
20
|
+
if (!source || source === window) return false;
|
|
21
|
+
for (const frame of document.querySelectorAll("iframe")) {
|
|
22
|
+
if (frame.contentWindow === source) return true;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
return Reflect.get(source, "parent") === window;
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
19
30
|
function useQuoteFromPostMessage() {
|
|
20
31
|
const { setQuote } = useQuote();
|
|
21
32
|
react.useEffect(() => {
|
|
@@ -46,7 +57,7 @@ function useQuoteFromPostMessage() {
|
|
|
46
57
|
};
|
|
47
58
|
const handleMessage = (event) => {
|
|
48
59
|
const data = event.data;
|
|
49
|
-
if (data && typeof data === "object" && data.type === "athena-add-to-composer" && typeof data.text === "string" && data.text.trim().length > 0) {
|
|
60
|
+
if (data && typeof data === "object" && data.type === "athena-add-to-composer" && typeof data.text === "string" && data.text.trim().length > 0 && isDirectChildFrame(event.source)) {
|
|
50
61
|
setQuote({
|
|
51
62
|
text: data.text,
|
|
52
63
|
sourceTitle: data.sourceTitle,
|
|
@@ -63,6 +74,7 @@ function useQuoteFromPostMessage() {
|
|
|
63
74
|
}, [setQuote]);
|
|
64
75
|
}
|
|
65
76
|
exports.QuoteProvider = QuoteProvider;
|
|
77
|
+
exports.isDirectChildFrame = isDirectChildFrame;
|
|
66
78
|
exports.useQuote = useQuote;
|
|
67
79
|
exports.useQuoteFromPostMessage = useQuoteFromPostMessage;
|
|
68
80
|
//# sourceMappingURL=QuoteContext.cjs.map
|