@tangle-network/agent-app 0.43.10 → 0.43.12

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.
@@ -36,7 +36,7 @@ import {
36
36
  } from '@tangle-network/agent-app/eval-campaign'
37
37
  ```
38
38
 
39
- > Requires `@tangle-network/agent-eval >= 0.95.0` (peer). The scaffold composes the substrate downward; never import a product package from agent-eval (layering rule).
39
+ > Requires `@tangle-network/agent-eval >= 0.100.0` (peer; current published is 0.107.0). The scaffold composes the substrate downward; never import a product package from agent-eval (layering rule).
40
40
 
41
41
  ## Minimal wiring (copy, then fill the three blanks)
42
42
 
@@ -89,7 +89,7 @@ if (result.gate.decision === 'ship') await ship(result.winnerSurface)
89
89
  | `baselineSurface` | — (required) | the surface the loop optimizes; start `''` |
90
90
  | `mutationPrimitives` | gepaProposer's own | your optimization levers (additive directives) |
91
91
  | `proposer` | `gepaProposer` | pass `evolutionaryProposer({ mutator })` for blind addendum rotation |
92
- | `gate` | `defaultProductionGate` (Δ 0.05) | `paretoSignificanceGate` for multi-objective; tune `deltaThreshold` for your rubric scale |
92
+ | `gate` | `defaultProductionGate` (Δ 0.05) | `paretoSignificanceGate` for multi-objective; tune `deltaThreshold` for your rubric scale . To prove a held-out lift is real CONTENT and not just added prompt/mount FOOTPRINT, compose `neutralizationGate` (agent-eval >= 0.107.0, from `@tangle-network/agent-eval/campaign`) — a footprint-matched placebo arm. It needs `ctx.neutralizedJudgeScores` from `runImprovementLoop({ neutralize })`; `selfImprove`/this scaffold do not surface that option yet, so wire it at the `runImprovementLoop` level or re-export it in the scaffold first. |
93
93
  | `budget` | 3 gens × pop 2, 0.25 holdout | `budget.reps` (replicates → tighter CIs), `budget.promoteTopK`, `budget.holdoutScenarios` (explicit split), `budget.dollars` (cost cap) |
94
94
  | `expectUsage` | **`'assert'`** | the fail-loud backend-integrity guard. Leave at `'assert'` for real runs (a stub cell throws); set `'off'` ONLY for a deterministic offline/replay run |
95
95
  | `labeledStore` | off | capture every artifact + judge score (the dataset you ship + few-shot corpus); set `captureSource` (default `'eval-run'`) |
@@ -11,7 +11,7 @@ You delegate the building to an agent holding `eval-architect` + `surface-evolut
11
11
 
12
12
  ## Invariant (non-negotiable)
13
13
 
14
- 1. **Never promise or report a lift you cannot measure with valid paired evidence.** Surface the honest verdict: `ship` / `hold` / `need-more-data` / `invalid`. "Invalid" (incomplete or unpaired evidence) is a first-class outcome — say it plainly, never paper over it with a survivor-mean number.
14
+ 1. **Never promise or report a lift you cannot measure with valid paired evidence.** Surface the honest verdict: `ship` / `hold` / `need-more-data` / `invalid`. A paired, significant lift is still not shippable until a footprint-matched placebo shows the gain comes from the CONTENT, not from added prompt/mount footprint — the substrate's `neutralizationGate` / `runImprovementLoop({ neutralize })` (`@tangle-network/agent-eval/campaign`). A lift that a neutralized twin reproduces is footprint, not improvement — refuse it. Route this check through `measurement-validation`. "Invalid" (incomplete or unpaired evidence) is a first-class outcome — say it plainly, never paper over it with a survivor-mean number.
15
15
  2. **Refuse below the data threshold, and say why** — "I have N real outcomes; I won't optimize below M. Here's how to get to M." A refusal with a reason builds more trust than a fabricated win.
16
16
  3. **Route correctly.** Improvable by surface-tuning → dispatch `surface-evolution`. Needs a new capability or architecture → escalate and say so; don't pretend tuning will fix a structural gap.
17
17
  4. **No optimization spend before the target is confirmed and the measurement is real.** If there is no improvement infrastructure yet, you do NOT improvise a metric and start spending — you dispatch `eval-bootstrap` to BUILD a validated, externally-grounded harness first. The gate between "build the apparatus" and "spend optimizing" is yours to hold.
@@ -13,7 +13,7 @@ Held by both the orchestrator (`improve-conductor`) and the builder (`eval-archi
13
13
 
14
14
  1. **Refuse to optimize if CV(metric) > the target delta.** If the run-to-run noise is bigger than the effect you're paying to move, the metric *cannot* validate the change — raise reps or fix the metric first. Do not tune against noise.
15
15
  2. **Refuse to report a lift over INCOMPLETE or UNPAIRED evidence.** Every held-out scenario must have a non-errored cell on *both* the baseline and the candidate side. Below the paired-n floor (≥3), the run is **invalid**, not a verdict. A lift computed over survivors is worse than no number.
16
- > **Enforced by** `trustVerdicts` from `@tangle-network/agent-app/eval-campaign` the after-gate: IRR floor + per-item rater spread (within-item, never pooled) + survivor floor, with each failed check named in `trustReasons`.
16
+ > **Enforced by** `trustVerdicts` from `@tangle-network/agent-app/eval-campaign` (rater-trust dimension: IRR floor + per-item rater spread, within-item never pooled, + survivor floor, each failure named in `trustReasons`), composed with the agent-eval statistical gates from `@tangle-network/agent-eval/campaign`: `powerPreflight` (before-gate for Invariant 1 — refuse to greenlight spend when the metric is underpowered vs the target delta), `heldOutGate` / `heldoutSignificance` (paired-bootstrap CI over the held-out split — the after-gate that would have caught the +47 by refusing an unpaired lift), and `neutralizationGate` + `neutralizeText` (footprint-matched placebo gate — a held-out lift that does not survive `neutralizeText` came from added prompt/mount FOOTPRINT, not content, and must not be believed).
17
17
  3. **Every metric ties to a product-value claim** — "if this number moves, *this* user-visible outcome moves with it." No claim → it's a proxy → don't optimize it.
18
18
  4. **Below the data threshold of real outcomes, refuse to optimize** — state N and say why. You cannot improve what you have not yet observed enough of.
19
19
 
@@ -24,7 +24,7 @@ Each skill's *judgment* surface is itself an evolvable surface, optimized by the
24
24
 
25
25
  > *Did following this skill produce an eval that yielded real held-out lift, with no critical-dimension regression?*
26
26
 
27
- Above a data threshold of real runs, the skill proposes revisions to its own judgment — gated identically (held-out, critical-dimension floor, paired-n ≥ floor). **Invariants are the frozen surface; judgment is the evolvable surface.** A skill improving itself is just `surface-evolution` pointed inward.
27
+ Above a data threshold of real runs, the skill proposes revisions to its own judgment — gated identically (held-out, critical-dimension floor, paired-n ≥ floor, and a footprint-matched placebo — the neutralization gate — that proves the lift came from content, not from added prompt/mount footprint). **Invariants are the frozen surface; judgment is the evolvable surface.** A skill improving itself is just `surface-evolution` pointed inward.
28
28
 
29
29
  ## The north-star: the agent builder, closed-loop
30
30
 
@@ -39,4 +39,4 @@ The fleet — **legal, tax, gtm, creative, insurance** — is the training distr
39
39
  - A skill that lists steps but has **no self-test** — you can't tell if following it worked.
40
40
  - An "invariant" that's really a **judgment call in disguise** — over-constraining; it should live in Judgment so the agent can adapt it per product.
41
41
  - A judgment surface with **no evolves-by hook** — it will rot, and nothing will notice.
42
- - A reported lift with **no held-out or no paired-n** — the slot machine. This is the one that ends the product.
42
+ - A reported lift with **no held-out or no paired-n, or no footprint-matched placebo (neutralization)** — the slot machine. This is the one that ends the product.
@@ -7,12 +7,12 @@ description: Optimize ONE evolvable surface (a prompt section / tool config) aga
7
7
 
8
8
  You are a closed-loop controller for agent quality. **Sensor** = the eval (built by `eval-architect`, certified by `measurement-validation`). **Controller** = the proposer that proposes surface rewrites. **Actuator** = promotion (writing the surface to the live agent). **Safety interlock** = the gate. The interlock is the entire point: it prefers *under-promotion* to Goodhart. A loop that ships every apparent gain is worthless; a loop that ships only evidence-backed gains is the product.
9
9
 
10
- The engine exists in the substrate (`@tangle-network/agent-eval/contract` `selfImprove` / `runImprovementLoop`, `gepaProposer`, `defaultProductionGate`) — re-exported via `@tangle-network/agent-app/eval-campaign`. **Do not rebuild it.** This skill is how you wire and run it safely.
10
+ The engine exists in the substrate (`@tangle-network/agent-eval/contract` `selfImprove` / `runImprovementLoop`, `gepaProposer`, `defaultProductionGate`, `neutralizationGate`, `composeGate`) — re-exported via `@tangle-network/agent-app/eval-campaign`. **Do not rebuild it.** This skill is how you wire and run it safely.
11
11
 
12
12
  ## Invariant (non-negotiable)
13
13
 
14
14
  1. **Optimize exactly ONE surface that production renders identically.** The artifact you mutate offline must be the artifact the live agent loads — one source, rendered both places (e.g. an evolvable prompt section materialized from a single file into the live system prompt). If offline and online diverge, the lift is fictional the moment it ships.
15
- 2. **Gate promotion on a held-out split AND a critical-dimension floor.** Never promote a net composite gain that regresses a guarded dimension (safety, hallucination, the regulated invariant). A +10 composite that loses 30 on hallucination is a regression, not a win.
15
+ 2. **Gate promotion on a held-out split AND a critical-dimension floor.** Never promote a net composite gain that regresses a guarded dimension (safety, hallucination, the regulated invariant). A +10 composite that loses 30 on hallucination is a regression, not a win. **AND gate on a footprint placebo.** A held-out lift proves the candidate beat baseline; it does NOT prove the lift came from the surface's CONTENT rather than the extra prompt/mount FOOTPRINT it added. Compose `neutralizationGate` (from `@tangle-network/agent-eval/campaign`, wired via `runImprovementLoop({ neutralize })`) AFTER the significance gate — `composeGate(heldOutGate({...}), neutralizationGate({...}))` — so a candidate whose content-blanked, footprint-matched twin reproduces more than `maxDecorativeFraction` (default 0.5) of the lift is HELD as decorative, no matter how large or significant its raw lift.
16
16
  3. **Budget is a hard ceiling and cost-aware** — skip cells beyond the ceiling, never abort. The user's spend maps to generations × candidates × reps: $0.20 buys one quick generation; $50 buys a wide search with tight CIs.
17
17
  4. **Never evolve a frozen surface.** The regulated invariants — human-in-the-loop, the compliance gate, auth/RBAC — are off-limits. Declare exactly what is evolvable; everything else the loop must not touch.
18
18
 
@@ -82,6 +82,19 @@ interface ConnectionRequirement {
82
82
  * app slug); the card then shows the requirement without a connect link. */
83
83
  connectUrl?: string | null;
84
84
  }
85
+ /**
86
+ * Outcome of a host-driven, in-place connect for a single proposal requirement
87
+ * (see {@link AssistantDockProps.onConnectRequirement}). The host owns the whole
88
+ * connect experience — an OAuth popup, an api-key entry modal, an app install,
89
+ * and any error surfacing — and the assistant stays agnostic to how it happens:
90
+ * the host reports back only whether the requirement is now satisfied, which the
91
+ * card uses to flip that requirement to connected without a reload. `connected:
92
+ * false` (a failed or cancelled connect) leaves the card unchanged so the user
93
+ * can retry.
94
+ */
95
+ interface ConnectRequirementResult {
96
+ connected: boolean;
97
+ }
85
98
  interface ToolProposalEventData {
86
99
  /** Null only if the server has no proposal store wired (tools then unusable). */
87
100
  proposalId: string | null;
@@ -428,6 +441,17 @@ interface UseAssistantChatOptions {
428
441
  * signal the platform used.
429
442
  */
430
443
  onWorkflowMutation?: () => void;
444
+ /**
445
+ * Called when the user activates a proposal requirement's connect affordance.
446
+ * The host runs whatever connect flow it owns — an OAuth popup, an api-key
447
+ * modal, an app install — and resolves whether the requirement is now
448
+ * satisfied; the card then flips that requirement to connected. Host-agnostic:
449
+ * the assistant never learns how the connection is made, so any host (platform,
450
+ * sandbox, intelligence, …) can supply its own. When omitted, the card falls
451
+ * back to navigating the requirement's connect target (see
452
+ * {@link ProposalCardProps.navigate}).
453
+ */
454
+ onConnectRequirement?: (requirement: ConnectionRequirement) => Promise<ConnectRequirementResult>;
431
455
  }
432
456
  interface AssistantChat {
433
457
  state: AssistantState;
@@ -441,6 +465,14 @@ interface AssistantChat {
441
465
  stop: () => void;
442
466
  confirm: (proposal: PendingProposal) => Promise<void>;
443
467
  cancel: (proposal: PendingProposal) => void;
468
+ /** True when the host supplied an in-place connect handler
469
+ * ({@link UseAssistantChatOptions.onConnectRequirement}); the proposal card
470
+ * uses this to offer in-place connect instead of navigating away. */
471
+ canConnectRequirement: boolean;
472
+ /** Run the host's in-place connect for one of a proposal's requirements and,
473
+ * on success, flip that requirement to connected on the card. Resolves (a
474
+ * no-op) when no host handler is set or the connect didn't complete. */
475
+ connectRequirement: (proposal: PendingProposal, requirement: ConnectionRequirement) => Promise<void>;
444
476
  reset: () => void;
445
477
  /** Open an existing thread from history, loading its transcript. */
446
478
  switchThread: (threadId: string) => void;
@@ -518,6 +550,12 @@ interface AssistantDockProps {
518
550
  renderGraph?: (yaml: string) => ReactNode;
519
551
  /** Called after a workflow-mutating tool is confirmed (host re-fetches its list). */
520
552
  onWorkflowMutation?: () => void;
553
+ /** In-place connect handler for a proposal's integration requirements. The host
554
+ * runs its own connect flow (OAuth popup, api-key modal, app install) and
555
+ * resolves whether the requirement is now satisfied; the proposal card then
556
+ * flips it to connected. Host-agnostic — when omitted, the card falls back to
557
+ * navigating the requirement's connect target via `navigate`. */
558
+ onConnectRequirement?: (requirement: ConnectionRequirement) => Promise<ConnectRequirementResult>;
521
559
  /** Markdown renderer for assistant message content (plain text when absent). */
522
560
  renderMarkdown?: (content: string) => ReactNode;
523
561
  /** Per-tool custom detail renderers for expanded tool cards in the transcript. */
@@ -527,7 +565,7 @@ interface AssistantDockProps {
527
565
  * transport, and proposal flow stay owned by the panel. */
528
566
  renderTranscript?: (view: AssistantTranscriptView) => ReactNode;
529
567
  }
530
- declare function AssistantDock({ userId, navigate, balanceUsd, formatMoney, renderGraph, onWorkflowMutation, renderMarkdown, toolRenderers, renderTranscript, }: AssistantDockProps): react.JSX.Element;
568
+ declare function AssistantDock({ userId, navigate, balanceUsd, formatMoney, renderGraph, onWorkflowMutation, onConnectRequirement, renderMarkdown, toolRenderers, renderTranscript, }: AssistantDockProps): react.JSX.Element;
531
569
 
532
570
  interface AssistantPanelProps {
533
571
  chat: AssistantChat;
@@ -608,11 +646,17 @@ interface ProposalCardProps {
608
646
  onCancel: () => void;
609
647
  /** Host navigation for connect targets / the integrations page. */
610
648
  navigate?: (path: string) => void;
649
+ /** In-place connect handler for a requirement. When provided, a requirement's
650
+ * connect affordance calls this (showing a busy state) instead of navigating
651
+ * to `connectUrl`/the integrations page — the host runs its own connect flow
652
+ * and the card flips the requirement to connected on success. Host-agnostic;
653
+ * wired by the panel from {@link UseAssistantChatOptions.onConnectRequirement}. */
654
+ onConnect?: (requirement: ConnectionRequirement) => void | Promise<void>;
611
655
  /** Render the workflow YAML as a node graph (the `./workflows` WorkflowGraph).
612
656
  * When absent, the YAML is shown as text. */
613
657
  renderGraph?: (yaml: string) => ReactNode;
614
658
  }
615
- declare function ProposalCard({ proposal, confirming, onConfirm, onCancel, navigate, renderGraph, }: ProposalCardProps): react.JSX.Element;
659
+ declare function ProposalCard({ proposal, confirming, onConfirm, onCancel, navigate, onConnect, renderGraph, }: ProposalCardProps): react.JSX.Element;
616
660
 
617
661
  interface AssistantLauncher {
618
662
  /** Whether the assistant drawer is open. */
@@ -630,4 +674,4 @@ declare function AssistantLauncherProvider({ children, }: {
630
674
  }): react.JSX.Element;
631
675
  declare function useAssistantLauncher(): AssistantLauncher;
632
676
 
633
- export { type AssistantChat, type AssistantClient, type AssistantClientConfig, AssistantClientInputError, AssistantClientProvider, type AssistantDeliveryMode, AssistantDock, type AssistantDockProps, type AssistantLauncher, AssistantLauncherProvider, type AssistantModelOption, type AssistantModels, type AssistantModelsResult, AssistantPanel, type AssistantPanelProps, type AssistantSendOptions, type AssistantStreamEvent, type AssistantThreadSummary, type AssistantThreads, AssistantTranscript, type AssistantTranscriptProps, type AssistantTranscriptView, type ChatMessage, type ChatRequest, type ChatRole, type ConfirmResult, type ConnectionRequirement, type ConnectionRequirementKind, type DeltaEventData, type DoneEventData, type ErrorEventData, type PendingProposal, ProposalCard, type ProposalCardProps, type ReasoningEventData, type ThreadEventData, type ThreadHistoryResult, type ToolActivityStatus, type ToolCallEventData, type ToolOutcome, type ToolProposalEventData, type ToolResultEventData, type UsageEventData, type UsageInfo, type UseAssistantChatOptions, adaptTranscript, assistantIsThinking, createAssistantClient, useAssistantChat, useAssistantClient, useAssistantLauncher, useAssistantModels, useAssistantThreads };
677
+ export { type AssistantChat, type AssistantClient, type AssistantClientConfig, AssistantClientInputError, AssistantClientProvider, type AssistantDeliveryMode, AssistantDock, type AssistantDockProps, type AssistantLauncher, AssistantLauncherProvider, type AssistantModelOption, type AssistantModels, type AssistantModelsResult, AssistantPanel, type AssistantPanelProps, type AssistantSendOptions, type AssistantStreamEvent, type AssistantThreadSummary, type AssistantThreads, AssistantTranscript, type AssistantTranscriptProps, type AssistantTranscriptView, type ChatMessage, type ChatRequest, type ChatRole, type ConfirmResult, type ConnectRequirementResult, type ConnectionRequirement, type ConnectionRequirementKind, type DeltaEventData, type DoneEventData, type ErrorEventData, type PendingProposal, ProposalCard, type ProposalCardProps, type ReasoningEventData, type ThreadEventData, type ThreadHistoryResult, type ToolActivityStatus, type ToolCallEventData, type ToolOutcome, type ToolProposalEventData, type ToolResultEventData, type UsageEventData, type UsageInfo, type UseAssistantChatOptions, adaptTranscript, assistantIsThinking, createAssistantClient, useAssistantChat, useAssistantClient, useAssistantLauncher, useAssistantModels, useAssistantThreads };
@@ -1029,6 +1029,25 @@ function assistantReducer(state, action) {
1029
1029
  status: pendingProposals.length > 0 ? "awaiting_confirm" : state.status === "awaiting_confirm" ? "idle" : state.status
1030
1030
  };
1031
1031
  }
1032
+ case "requirement_connected": {
1033
+ const kind = action.kind ?? "integration";
1034
+ let changed = false;
1035
+ const pendingProposals = state.pendingProposals.map((p) => {
1036
+ if (p.callId !== action.callId || !p.requirements) return p;
1037
+ let rowChanged = false;
1038
+ const requirements = p.requirements.map((r) => {
1039
+ if (!r.connected && r.provider === action.provider && (r.kind ?? "integration") === kind) {
1040
+ rowChanged = true;
1041
+ return { ...r, connected: true };
1042
+ }
1043
+ return r;
1044
+ });
1045
+ if (!rowChanged) return p;
1046
+ changed = true;
1047
+ return { ...p, requirements };
1048
+ });
1049
+ return changed ? { ...state, pendingProposals } : state;
1050
+ }
1032
1051
  case "stopped":
1033
1052
  return {
1034
1053
  ...state,
@@ -1121,6 +1140,8 @@ function useAssistantChat(userId, options) {
1121
1140
  clientRef.current = client;
1122
1141
  const onWorkflowMutationRef = useRef(options?.onWorkflowMutation);
1123
1142
  onWorkflowMutationRef.current = options?.onWorkflowMutation;
1143
+ const onConnectRequirementRef = useRef(options?.onConnectRequirement);
1144
+ onConnectRequirementRef.current = options?.onConnectRequirement;
1124
1145
  const confirmingRef = useRef(/* @__PURE__ */ new Set());
1125
1146
  const [confirmingIds, setConfirmingIds] = useState(EMPTY_IDS);
1126
1147
  const sendingRef = useRef(false);
@@ -1362,6 +1383,29 @@ function useAssistantChat(userId, options) {
1362
1383
  error: null
1363
1384
  });
1364
1385
  }, []);
1386
+ const connectRequirement = useCallback(
1387
+ async (proposal, requirement) => {
1388
+ const handler = onConnectRequirementRef.current;
1389
+ if (!handler) return;
1390
+ const seq = confirmSeqRef.current;
1391
+ let result;
1392
+ try {
1393
+ result = await handler(requirement);
1394
+ } catch {
1395
+ return;
1396
+ }
1397
+ if (confirmSeqRef.current !== seq) return;
1398
+ if (result?.connected) {
1399
+ dispatch({
1400
+ type: "requirement_connected",
1401
+ callId: proposal.callId,
1402
+ provider: requirement.provider,
1403
+ kind: requirement.kind
1404
+ });
1405
+ }
1406
+ },
1407
+ []
1408
+ );
1365
1409
  const reset = useCallback(() => {
1366
1410
  streamSeqRef.current += 1;
1367
1411
  confirmSeqRef.current += 1;
@@ -1454,6 +1498,8 @@ function useAssistantChat(userId, options) {
1454
1498
  stop,
1455
1499
  confirm,
1456
1500
  cancel,
1501
+ canConnectRequirement: Boolean(options?.onConnectRequirement),
1502
+ connectRequirement,
1457
1503
  reset,
1458
1504
  switchThread,
1459
1505
  restoring
@@ -1599,13 +1645,13 @@ function useAssistantThreads(userId) {
1599
1645
  // src/assistant/AssistantDock.tsx
1600
1646
  import { MessageSquare } from "lucide-react";
1601
1647
  import {
1602
- useEffect as useEffect6,
1603
- useRef as useRef7
1648
+ useEffect as useEffect7,
1649
+ useRef as useRef8
1604
1650
  } from "react";
1605
1651
 
1606
1652
  // src/assistant/AssistantPanel.tsx
1607
1653
  import { History, MessageSquarePlus, Minus, Plus, X } from "lucide-react";
1608
- import { useEffect as useEffect5, useMemo as useMemo3, useRef as useRef5, useState as useState6 } from "react";
1654
+ import { useEffect as useEffect6, useMemo as useMemo3, useRef as useRef6, useState as useState6 } from "react";
1609
1655
 
1610
1656
  // src/assistant/AssistantHistory.tsx
1611
1657
  import { Search, Trash2 } from "lucide-react";
@@ -1725,7 +1771,7 @@ function AssistantHistory({
1725
1771
  }
1726
1772
 
1727
1773
  // src/assistant/ProposalCard.tsx
1728
- import { useState as useState4 } from "react";
1774
+ import { useEffect as useEffect4, useRef as useRef3, useState as useState4 } from "react";
1729
1775
 
1730
1776
  // src/assistant/provider-label.ts
1731
1777
  var PROVIDER_LABELS = {
@@ -1743,13 +1789,14 @@ function providerLabel(provider) {
1743
1789
  }
1744
1790
 
1745
1791
  // src/assistant/ProposalCard.tsx
1746
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1792
+ import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1747
1793
  function ProposalCard({
1748
1794
  proposal,
1749
1795
  confirming,
1750
1796
  onConfirm,
1751
1797
  onCancel,
1752
1798
  navigate,
1799
+ onConnect,
1753
1800
  renderGraph
1754
1801
  }) {
1755
1802
  const view = describeProposal(proposal);
@@ -1805,7 +1852,8 @@ function ProposalCard({
1805
1852
  RequirementRow,
1806
1853
  {
1807
1854
  req: r,
1808
- navigate
1855
+ navigate,
1856
+ onConnect
1809
1857
  },
1810
1858
  `${r.provider}-${r.kind ?? "integration"}`
1811
1859
  )) }),
@@ -1855,14 +1903,41 @@ function openConnect(target, navigate) {
1855
1903
  }
1856
1904
  function RequirementRow({
1857
1905
  req,
1858
- navigate
1906
+ navigate,
1907
+ onConnect
1859
1908
  }) {
1909
+ const [connecting, setConnecting] = useState4(false);
1910
+ const mountedRef = useRef3(true);
1911
+ useEffect4(() => {
1912
+ mountedRef.current = true;
1913
+ return () => {
1914
+ mountedRef.current = false;
1915
+ };
1916
+ }, []);
1860
1917
  const label = providerLabel(req.provider);
1861
1918
  const isApp = req.kind === "github_app";
1862
1919
  const kindLabel = isApp ? `${label} App` : label;
1863
1920
  const statusText = req.connected ? isApp ? "installed" : "connected" : isApp ? "not installed" : "not connected";
1864
- const canConnect = !req.connected && req.connectUrl !== null;
1921
+ const canConnect = !req.connected && (Boolean(onConnect) || req.connectUrl !== null);
1865
1922
  const target = req.connectUrl ?? "/app/integrations";
1923
+ const handleConnect = () => {
1924
+ if (!onConnect) {
1925
+ openConnect(target, navigate);
1926
+ return;
1927
+ }
1928
+ setConnecting(true);
1929
+ let pending;
1930
+ try {
1931
+ pending = onConnect(req);
1932
+ } catch {
1933
+ if (mountedRef.current) setConnecting(false);
1934
+ return;
1935
+ }
1936
+ void Promise.resolve(pending).catch(() => {
1937
+ }).finally(() => {
1938
+ if (mountedRef.current) setConnecting(false);
1939
+ });
1940
+ };
1866
1941
  return /* @__PURE__ */ jsxs2("li", { className: "flex items-center justify-between gap-2 text-xs", children: [
1867
1942
  /* @__PURE__ */ jsxs2("span", { className: "flex min-w-0 items-center gap-2", children: [
1868
1943
  /* @__PURE__ */ jsx3(ProviderLogo, { provider: req.provider, size: 16 }),
@@ -1884,16 +1959,17 @@ function RequirementRow({
1884
1959
  )
1885
1960
  ] })
1886
1961
  ] }),
1887
- canConnect && /* @__PURE__ */ jsxs2(
1962
+ canConnect && /* @__PURE__ */ jsx3(
1888
1963
  "button",
1889
1964
  {
1890
1965
  type: "button",
1891
- onClick: () => openConnect(target, navigate),
1892
- className: "shrink-0 text-primary",
1893
- children: [
1966
+ onClick: handleConnect,
1967
+ disabled: connecting,
1968
+ className: "shrink-0 text-primary disabled:opacity-50",
1969
+ children: connecting ? isApp ? "Installing\u2026" : "Connecting\u2026" : /* @__PURE__ */ jsxs2(Fragment, { children: [
1894
1970
  isApp ? "Install" : "Connect",
1895
1971
  " \u2192"
1896
- ]
1972
+ ] })
1897
1973
  }
1898
1974
  )
1899
1975
  ] });
@@ -1901,7 +1977,7 @@ function RequirementRow({
1901
1977
 
1902
1978
  // src/assistant/transcript.tsx
1903
1979
  import { useCallback as useCallback3, useMemo as useMemo2 } from "react";
1904
- import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1980
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1905
1981
  function assistantIsThinking(state) {
1906
1982
  if (state.status !== "streaming") return false;
1907
1983
  const streaming = state.streamingId ? state.messages.find((m) => m.id === state.streamingId) : void 0;
@@ -1999,7 +2075,7 @@ function ProposalSlot({
1999
2075
  proposal,
2000
2076
  render
2001
2077
  }) {
2002
- return /* @__PURE__ */ jsx4(Fragment, { children: render(proposal) });
2078
+ return /* @__PURE__ */ jsx4(Fragment2, { children: render(proposal) });
2003
2079
  }
2004
2080
  function AssistantTranscript({
2005
2081
  view,
@@ -2016,7 +2092,7 @@ function AssistantTranscript({
2016
2092
  [renderMarkdown]
2017
2093
  );
2018
2094
  if (messages.length === 0 && !view.isStreaming) {
2019
- return /* @__PURE__ */ jsx4(Fragment, { children: emptyState });
2095
+ return /* @__PURE__ */ jsx4(Fragment2, { children: emptyState });
2020
2096
  }
2021
2097
  return /* @__PURE__ */ jsx4(
2022
2098
  ChatMessages,
@@ -2026,7 +2102,7 @@ function AssistantTranscript({
2026
2102
  agentLabel: "Assistant",
2027
2103
  renderMarkdown: markdown,
2028
2104
  toolRenderers,
2029
- renderEmpty: () => /* @__PURE__ */ jsx4(Fragment, { children: emptyState }),
2105
+ renderEmpty: () => /* @__PURE__ */ jsx4(Fragment2, { children: emptyState }),
2030
2106
  renderExtras: (message) => {
2031
2107
  const proposals = message.id === proposalHostId && view.pendingProposals.length > 0 ? /* @__PURE__ */ jsx4("div", { className: "mt-3 flex flex-col gap-3", children: view.pendingProposals.map((proposal) => /* @__PURE__ */ jsx4(
2032
2108
  ProposalSlot,
@@ -2041,7 +2117,7 @@ function AssistantTranscript({
2041
2117
  " this turn"
2042
2118
  ] }) : null;
2043
2119
  if (!proposals && !cost) return null;
2044
- return /* @__PURE__ */ jsxs3(Fragment, { children: [
2120
+ return /* @__PURE__ */ jsxs3(Fragment2, { children: [
2045
2121
  proposals,
2046
2122
  cost
2047
2123
  ] });
@@ -2051,7 +2127,7 @@ function AssistantTranscript({
2051
2127
  }
2052
2128
 
2053
2129
  // src/assistant/usePanelPrefs.ts
2054
- import { useCallback as useCallback4, useEffect as useEffect4, useRef as useRef3, useState as useState5 } from "react";
2130
+ import { useCallback as useCallback4, useEffect as useEffect5, useRef as useRef4, useState as useState5 } from "react";
2055
2131
  var MIN_PANEL_WIDTH = 360;
2056
2132
  var DEFAULT_PANEL_WIDTH = 448;
2057
2133
  var MAX_PANEL_WIDTH_FRACTION = 0.95;
@@ -2094,8 +2170,8 @@ function clampScale(value) {
2094
2170
  function usePanelWidth() {
2095
2171
  const [width, setWidthState] = useState5(DEFAULT_PANEL_WIDTH);
2096
2172
  const [maxWidth, setMaxWidth] = useState5(() => maxPanelWidth());
2097
- const desiredRef = useRef3(DEFAULT_PANEL_WIDTH);
2098
- useEffect4(() => {
2173
+ const desiredRef = useRef4(DEFAULT_PANEL_WIDTH);
2174
+ useEffect5(() => {
2099
2175
  setMaxWidth(maxPanelWidth());
2100
2176
  const stored = readNumber(WIDTH_KEY);
2101
2177
  if (stored != null) {
@@ -2103,7 +2179,7 @@ function usePanelWidth() {
2103
2179
  setWidthState(clampWidth(stored));
2104
2180
  }
2105
2181
  }, []);
2106
- useEffect4(() => {
2182
+ useEffect5(() => {
2107
2183
  const onResize = () => {
2108
2184
  setMaxWidth(maxPanelWidth());
2109
2185
  setWidthState(clampWidth(desiredRef.current));
@@ -2130,7 +2206,7 @@ function usePanelWidth() {
2130
2206
  }
2131
2207
  function useFontScale() {
2132
2208
  const [scale, setScaleState] = useState5(DEFAULT_FONT_SCALE);
2133
- useEffect4(() => {
2209
+ useEffect5(() => {
2134
2210
  const stored = readNumber(FONT_SCALE_KEY);
2135
2211
  if (stored != null) setScaleState(clampScale(stored));
2136
2212
  }, []);
@@ -2153,7 +2229,7 @@ function useIsDesktop() {
2153
2229
  const [isDesktop, setIsDesktop] = useState5(
2154
2230
  () => typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia("(min-width: 768px)").matches : true
2155
2231
  );
2156
- useEffect4(() => {
2232
+ useEffect5(() => {
2157
2233
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
2158
2234
  return;
2159
2235
  }
@@ -2167,12 +2243,12 @@ function useIsDesktop() {
2167
2243
  }
2168
2244
 
2169
2245
  // src/assistant/use-stick-to-bottom.ts
2170
- import { useCallback as useCallback5, useLayoutEffect, useRef as useRef4 } from "react";
2246
+ import { useCallback as useCallback5, useLayoutEffect, useRef as useRef5 } from "react";
2171
2247
  var STICK_SLACK_PX = 48;
2172
2248
  function useStickToBottom(ref, { enabled, contentSignature, streamingId, threadId }) {
2173
- const stuckRef = useRef4(true);
2174
- const prevStreamingRef = useRef4(streamingId);
2175
- const prevThreadRef = useRef4(threadId);
2249
+ const stuckRef = useRef5(true);
2250
+ const prevStreamingRef = useRef5(streamingId);
2251
+ const prevThreadRef = useRef5(threadId);
2176
2252
  const onScroll = useCallback5(() => {
2177
2253
  if (!enabled) return;
2178
2254
  const el = ref.current;
@@ -2250,24 +2326,24 @@ function AssistantPanel({
2250
2326
  const threads = useAssistantThreads(userId);
2251
2327
  const font = useFontScale();
2252
2328
  const [view, setView] = useState6("chat");
2253
- const historyButtonRef = useRef5(null);
2254
- const logRef = useRef5(null);
2329
+ const historyButtonRef = useRef6(null);
2330
+ const logRef = useRef6(null);
2255
2331
  const pickerModels = useMemo3(
2256
2332
  () => toPickerModels(models, chat.selectedModel),
2257
2333
  [models, chat.selectedModel]
2258
2334
  );
2259
2335
  const pickerValue = chat.selectedModel ?? models.default ?? "";
2260
2336
  const { state } = chat;
2261
- const chatRef = useRef5(chat);
2337
+ const chatRef = useRef6(chat);
2262
2338
  chatRef.current = chat;
2263
- useEffect5(() => {
2339
+ useEffect6(() => {
2264
2340
  if (view !== "history") return;
2265
2341
  const search = logRef.current?.querySelector(
2266
2342
  'input[type="search"]'
2267
2343
  );
2268
2344
  (search ?? logRef.current)?.focus();
2269
2345
  }, [view]);
2270
- useEffect5(() => {
2346
+ useEffect6(() => {
2271
2347
  if (view !== "history") return;
2272
2348
  const onKeyDownCapture = (e) => {
2273
2349
  if (e.key !== "Escape") return;
@@ -2317,6 +2393,7 @@ function AssistantPanel({
2317
2393
  onConfirm: () => chat.confirm(proposal),
2318
2394
  onCancel: () => chat.cancel(proposal),
2319
2395
  navigate,
2396
+ onConnect: chat.canConnectRequirement ? (requirement) => chat.connectRequirement(proposal, requirement) : void 0,
2320
2397
  renderGraph
2321
2398
  }
2322
2399
  );
@@ -2617,7 +2694,7 @@ function useAssistantLauncher() {
2617
2694
  }
2618
2695
 
2619
2696
  // src/assistant/ResizeHandle.tsx
2620
- import { useRef as useRef6 } from "react";
2697
+ import { useRef as useRef7 } from "react";
2621
2698
  import { jsx as jsx7 } from "react/jsx-runtime";
2622
2699
  function ResizeHandle({
2623
2700
  width,
@@ -2626,7 +2703,7 @@ function ResizeHandle({
2626
2703
  onCommit,
2627
2704
  onNudge
2628
2705
  }) {
2629
- const dragRef = useRef6(null);
2706
+ const dragRef = useRef7(null);
2630
2707
  const onPointerDown = (e) => {
2631
2708
  if (e.button !== 0) return;
2632
2709
  e.preventDefault();
@@ -2695,7 +2772,7 @@ function ResizeHandle({
2695
2772
  }
2696
2773
 
2697
2774
  // src/assistant/AssistantDock.tsx
2698
- import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
2775
+ import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
2699
2776
  function focusableWithin(container) {
2700
2777
  const selector = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
2701
2778
  return Array.from(container.querySelectorAll(selector)).filter(
@@ -2709,19 +2786,23 @@ function AssistantDock({
2709
2786
  formatMoney,
2710
2787
  renderGraph,
2711
2788
  onWorkflowMutation,
2789
+ onConnectRequirement,
2712
2790
  renderMarkdown,
2713
2791
  toolRenderers,
2714
2792
  renderTranscript
2715
2793
  }) {
2716
2794
  const { open, openAssistant, closeAssistant } = useAssistantLauncher();
2717
- const chat = useAssistantChat(userId, { onWorkflowMutation });
2795
+ const chat = useAssistantChat(userId, {
2796
+ onWorkflowMutation,
2797
+ onConnectRequirement
2798
+ });
2718
2799
  const isDesktop = useIsDesktop();
2719
2800
  const { width, maxWidth, setWidth, previewWidth, nudgeWidth } = usePanelWidth();
2720
- const launcherRef = useRef7(null);
2721
- const dialogRef = useRef7(null);
2722
- const returnFocusRef = useRef7(null);
2723
- const wasOpenRef = useRef7(false);
2724
- useEffect6(() => {
2801
+ const launcherRef = useRef8(null);
2802
+ const dialogRef = useRef8(null);
2803
+ const returnFocusRef = useRef8(null);
2804
+ const wasOpenRef = useRef8(false);
2805
+ useEffect7(() => {
2725
2806
  if (!open) return;
2726
2807
  const onKeyDown = (e) => {
2727
2808
  if (e.key === "Escape") closeAssistant();
@@ -2729,7 +2810,7 @@ function AssistantDock({
2729
2810
  document.addEventListener("keydown", onKeyDown);
2730
2811
  return () => document.removeEventListener("keydown", onKeyDown);
2731
2812
  }, [open, closeAssistant]);
2732
- useEffect6(() => {
2813
+ useEffect7(() => {
2733
2814
  if (open) {
2734
2815
  if (!wasOpenRef.current && !returnFocusRef.current) {
2735
2816
  returnFocusRef.current = document.activeElement;
@@ -2785,7 +2866,7 @@ function AssistantDock({
2785
2866
  first.focus();
2786
2867
  }
2787
2868
  };
2788
- return /* @__PURE__ */ jsxs5(Fragment2, { children: [
2869
+ return /* @__PURE__ */ jsxs5(Fragment3, { children: [
2789
2870
  /* @__PURE__ */ jsx8(
2790
2871
  "div",
2791
2872
  {