@tangle-network/agent-app 0.60.172 → 0.60.174

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/dist/assistant/index.js +4 -4
  2. package/dist/chat-react/index.js +3 -3
  3. package/dist/chat-routes/index.js +2 -2
  4. package/dist/{chunk-KICTB6RK.js → chunk-F2BXNZAH.js} +2 -2
  5. package/dist/{chunk-CRHXK3QT.js → chunk-GYQTJA6N.js} +3 -3
  6. package/dist/{chunk-PZHZV6QY.js → chunk-PP5KIVHL.js} +2 -2
  7. package/dist/{chunk-RCRC2JKP.js → chunk-QW4UOEPH.js} +2 -2
  8. package/dist/{chunk-MXNVSY6J.js → chunk-VBRUG4N5.js} +47 -12
  9. package/dist/chunk-VBRUG4N5.js.map +1 -0
  10. package/dist/harness/index.d.ts +33 -74
  11. package/dist/harness/index.js +3 -1
  12. package/dist/harness/session-selection.d.ts +35 -0
  13. package/dist/integrations-react/index.d.ts +1 -1
  14. package/dist/integrations-react/index.js +18 -14
  15. package/dist/integrations-react/index.js.map +1 -1
  16. package/dist/integrations-react/panel.d.ts +3 -1
  17. package/dist/integrations-react/popup.d.ts +1 -1
  18. package/dist/integrations-react/projection.d.ts +15 -3
  19. package/dist/integrations-react/use-hub-integrations.d.ts +4 -2
  20. package/dist/sandbox/index.js +2 -2
  21. package/dist/skills-placement/index.js +1 -1
  22. package/dist/web-react/agent-profile-editor.js +3 -3
  23. package/dist/web-react/index.js +4 -4
  24. package/package.json +1 -1
  25. package/dist/chunk-MXNVSY6J.js.map +0 -1
  26. /package/dist/{chunk-KICTB6RK.js.map → chunk-F2BXNZAH.js.map} +0 -0
  27. /package/dist/{chunk-CRHXK3QT.js.map → chunk-GYQTJA6N.js.map} +0 -0
  28. /package/dist/{chunk-PZHZV6QY.js.map → chunk-PP5KIVHL.js.map} +0 -0
  29. /package/dist/{chunk-RCRC2JKP.js.map → chunk-QW4UOEPH.js.map} +0 -0
@@ -8,15 +8,15 @@ import "../chunk-FBVLEGEG.js";
8
8
  import "../chunk-CW6F6GDP.js";
9
9
  import "../chunk-ILZQAXJH.js";
10
10
  import "../chunk-E4AY62T2.js";
11
- import "../chunk-CRHXK3QT.js";
11
+ import "../chunk-GYQTJA6N.js";
12
12
  import "../chunk-Y6PRDUFJ.js";
13
13
  import "../chunk-GEYACSFW.js";
14
14
  import {
15
15
  ChatComposer
16
- } from "../chunk-PZHZV6QY.js";
16
+ } from "../chunk-PP5KIVHL.js";
17
17
  import "../chunk-L7MB2LLM.js";
18
18
  import "../chunk-SJWIZT7B.js";
19
- import "../chunk-KICTB6RK.js";
19
+ import "../chunk-F2BXNZAH.js";
20
20
  import "../chunk-R5K5ML3Q.js";
21
21
  import {
22
22
  ModelPicker
@@ -27,7 +27,7 @@ import {
27
27
  } from "../chunk-LSUELMHY.js";
28
28
  import "../chunk-FQYAJYPW.js";
29
29
  import "../chunk-WTC6AOPL.js";
30
- import "../chunk-MXNVSY6J.js";
30
+ import "../chunk-VBRUG4N5.js";
31
31
  import "../chunk-B7CL7ENS.js";
32
32
  import "../chunk-PV33JZKV.js";
33
33
  import "../chunk-AMUXRAUO.js";
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  ComposerModeControls,
3
3
  EntryComposer
4
- } from "../chunk-PZHZV6QY.js";
4
+ } from "../chunk-PP5KIVHL.js";
5
5
  import "../chunk-L7MB2LLM.js";
6
6
  import "../chunk-SJWIZT7B.js";
7
- import "../chunk-KICTB6RK.js";
7
+ import "../chunk-F2BXNZAH.js";
8
8
  import "../chunk-R5K5ML3Q.js";
9
9
  import "../chunk-AA4FKEQW.js";
10
10
  import "../chunk-RASGKEAL.js";
11
11
  import "../chunk-WTC6AOPL.js";
12
- import "../chunk-MXNVSY6J.js";
12
+ import "../chunk-VBRUG4N5.js";
13
13
  import "../chunk-AMUXRAUO.js";
14
14
  import "../chunk-YHRWZZJN.js";
15
15
  import "../chunk-TXD5HXLE.js";
@@ -42,9 +42,9 @@ import {
42
42
  flattenHistory,
43
43
  readSandboxBinaryBytes,
44
44
  statSandboxFileSize
45
- } from "../chunk-RCRC2JKP.js";
45
+ } from "../chunk-QW4UOEPH.js";
46
46
  import "../chunk-4LBCTFBP.js";
47
- import "../chunk-MXNVSY6J.js";
47
+ import "../chunk-VBRUG4N5.js";
48
48
  import "../chunk-LWSJK546.js";
49
49
  import "../chunk-74WXGIEK.js";
50
50
  import "../chunk-6A7MYOUI.js";
@@ -15,7 +15,7 @@ import {
15
15
  import {
16
16
  snapHarnessToModel,
17
17
  snapModelToHarness
18
- } from "./chunk-MXNVSY6J.js";
18
+ } from "./chunk-VBRUG4N5.js";
19
19
 
20
20
  // src/web-react/composer-file-accept.ts
21
21
  function isAcceptedFileType(file, accept) {
@@ -576,4 +576,4 @@ export {
576
576
  HarnessPicker,
577
577
  AgentSessionControls
578
578
  };
579
- //# sourceMappingURL=chunk-KICTB6RK.js.map
579
+ //# sourceMappingURL=chunk-F2BXNZAH.js.map
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  HarnessPicker,
3
3
  filterAcceptedFiles
4
- } from "./chunk-KICTB6RK.js";
4
+ } from "./chunk-F2BXNZAH.js";
5
5
  import {
6
6
  EffortPicker,
7
7
  ModelPicker,
@@ -13,7 +13,7 @@ import {
13
13
  import {
14
14
  isModelCompatibleWithHarness,
15
15
  snapModelToHarness
16
- } from "./chunk-MXNVSY6J.js";
16
+ } from "./chunk-VBRUG4N5.js";
17
17
  import {
18
18
  isCommitSha,
19
19
  parseGitHubLocation,
@@ -2214,4 +2214,4 @@ export {
2214
2214
  AGENT_PROFILE_EDITOR_SECTIONS,
2215
2215
  AgentProfileEditor
2216
2216
  };
2217
- //# sourceMappingURL=chunk-CRHXK3QT.js.map
2217
+ //# sourceMappingURL=chunk-GYQTJA6N.js.map
@@ -10,7 +10,7 @@ import {
10
10
  filterAcceptedFiles,
11
11
  isAcceptedFileType,
12
12
  renamePastedImages
13
- } from "./chunk-KICTB6RK.js";
13
+ } from "./chunk-F2BXNZAH.js";
14
14
  import {
15
15
  OVERLAY_SHADOW,
16
16
  POPOVER_OPTION_FOCUS,
@@ -1552,4 +1552,4 @@ export {
1552
1552
  ComposerModeControls,
1553
1553
  EntryComposer
1554
1554
  };
1555
- //# sourceMappingURL=chunk-PZHZV6QY.js.map
1555
+ //# sourceMappingURL=chunk-PP5KIVHL.js.map
@@ -13,7 +13,7 @@ import {
13
13
  DEFAULT_HARNESS,
14
14
  assertHarnessModelCompatible,
15
15
  isHarness
16
- } from "./chunk-MXNVSY6J.js";
16
+ } from "./chunk-VBRUG4N5.js";
17
17
  import {
18
18
  assertProfilePromptWithinBudget,
19
19
  fingerprintAgentProfile
@@ -3343,4 +3343,4 @@ export {
3343
3343
  isTerminalPromptEvent,
3344
3344
  detectInteractiveQuestion
3345
3345
  };
3346
- //# sourceMappingURL=chunk-RCRC2JKP.js.map
3346
+ //# sourceMappingURL=chunk-QW4UOEPH.js.map
@@ -8,6 +8,50 @@ import {
8
8
  snapHarnessToModel as aiSnapHarnessToModel,
9
9
  snapModelToHarness as aiSnapModelToHarness
10
10
  } from "@tangle-network/agent-interface";
11
+
12
+ // src/harness/session-selection.ts
13
+ var HarnessSelectionError = class extends Error {
14
+ code;
15
+ harness;
16
+ source;
17
+ constructor(code, value, source) {
18
+ const harness = typeof value === "string" ? value : null;
19
+ super(`${code}: ${source} harness ${harness === null ? "<invalid>" : JSON.stringify(harness)}`);
20
+ this.name = "HarnessSelectionError";
21
+ this.code = code;
22
+ this.harness = harness;
23
+ this.source = source;
24
+ }
25
+ };
26
+ function selectSessionHarness(input, knownHarnesses, defaultHarness) {
27
+ const known = new Set(knownHarnesses);
28
+ const resolve = (value2, source2) => {
29
+ if (typeof value2 !== "string" || !known.has(value2)) {
30
+ throw new HarnessSelectionError("harness_unknown", value2, source2);
31
+ }
32
+ return value2;
33
+ };
34
+ const admit = (harness2, source2, existing) => {
35
+ if (input.availability === void 0) return;
36
+ const allowed = existing ? input.availability.existingSessions ?? input.availability.newSessions : input.availability.newSessions;
37
+ if (!allowed.includes(harness2)) {
38
+ throw new HarnessSelectionError("harness_unavailable", harness2, source2);
39
+ }
40
+ };
41
+ if (input.sessionHarness !== void 0 && input.sessionHarness !== null) {
42
+ const harness2 = resolve(input.sessionHarness, "session");
43
+ admit(harness2, "session", true);
44
+ const requested = input.requested === void 0 || input.requested === null ? void 0 : resolve(input.requested, "requested");
45
+ return { harness: harness2, locked: true, swapAttempted: requested !== void 0 && requested !== harness2 };
46
+ }
47
+ const source = input.requested !== void 0 && input.requested !== null ? "requested" : input.workspaceDefault !== void 0 && input.workspaceDefault !== null ? "workspace" : "fallback";
48
+ const value = source === "requested" ? input.requested : source === "workspace" ? input.workspaceDefault : input.fallback ?? defaultHarness;
49
+ const harness = resolve(value, source);
50
+ admit(harness, source, false);
51
+ return { harness, locked: false, swapAttempted: false };
52
+ }
53
+
54
+ // src/harness/index.ts
11
55
  var NON_BACKEND_HARNESSES = ["gemini"];
12
56
  var nonBackendHarnesses = new Set(NON_BACKEND_HARNESSES);
13
57
  var KNOWN_HARNESSES = [
@@ -24,14 +68,7 @@ function coerceHarness(value, fallback = DEFAULT_HARNESS) {
24
68
  return isHarness(value) ? value : fallback;
25
69
  }
26
70
  function resolveSessionHarness(input = {}) {
27
- const fallback = input.fallback ?? DEFAULT_HARNESS;
28
- if (isHarness(input.sessionHarness)) {
29
- const locked = input.sessionHarness;
30
- const swapAttempted = isHarness(input.requested) && input.requested !== locked;
31
- return { harness: locked, locked: true, swapAttempted };
32
- }
33
- const harness = coerceHarness(input.requested, coerceHarness(input.workspaceDefault, fallback));
34
- return { harness, locked: false, swapAttempted: false };
71
+ return selectSessionHarness(input, KNOWN_HARNESSES, DEFAULT_HARNESS);
35
72
  }
36
73
  function configHarness(value, where) {
37
74
  if (!isHarness(value)) throw new Error(`${where} names an unknown harness: ${String(value)}`);
@@ -93,9 +130,6 @@ var HARNESS_CAPABILITIES = {
93
130
  prime: { subagents: true, mcp: "native", toolControls: "runtime", skillsDir: ".prime/agent/skills" },
94
131
  hermes: { subagents: false, mcp: "native", toolControls: "runtime", skillsDir: null },
95
132
  openclaw: { subagents: false, mcp: "config-file", toolControls: "runtime", skillsDir: "skills" },
96
- // Harnesses the platform placement map does not bridge yet: conservative
97
- // facts — assert nothing they have not proven. Each upgrades to its real
98
- // facts when the materializer grows support for it.
99
133
  acp: { subagents: false, mcp: "native", toolControls: "runtime", skillsDir: null },
100
134
  amp: { subagents: false, mcp: "native", toolControls: "runtime", skillsDir: null },
101
135
  "factory-droids": { subagents: false, mcp: "native", toolControls: "runtime", skillsDir: null },
@@ -110,6 +144,7 @@ function harnessCapabilities(harness) {
110
144
  }
111
145
 
112
146
  export {
147
+ HarnessSelectionError,
113
148
  modelProvider,
114
149
  KNOWN_HARNESSES,
115
150
  DEFAULT_HARNESS,
@@ -123,4 +158,4 @@ export {
123
158
  assertHarnessModelCompatible,
124
159
  harnessCapabilities
125
160
  };
126
- //# sourceMappingURL=chunk-MXNVSY6J.js.map
161
+ //# sourceMappingURL=chunk-VBRUG4N5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/harness/index.ts","../src/harness/session-selection.ts"],"sourcesContent":["/**\n * Coding-agent harness selection, model compatibility, and the session lock.\n *\n * Known identities are not a deployment's available runners. Products supply\n * server-owned availability to resolveSessionHarness. Historical session IDs\n * remain intact when their runner is unavailable; a harness change requires an\n * explicit transfer, not a fallback or a rewritten session lock.\n *\n * Model compatibility is owned by @tangle-network/agent-interface. The shell\n * owns session selection; the consumer owns storage and deployment policy.\n */\n\nimport {\n harnessProviders,\n harnessSupportsModel,\n harnessTypeSchema,\n modelProvider,\n preferredHarnessForModel,\n snapHarnessToModel as aiSnapHarnessToModel,\n snapModelToHarness as aiSnapModelToHarness,\n type HarnessType,\n} from '@tangle-network/agent-interface'\nimport {\n selectSessionHarness,\n type HarnessAvailability as SelectionAvailability,\n type SessionHarnessSelection,\n type SessionHarnessSelectionInput,\n} from './session-selection'\n\nexport { HarnessSelectionError } from './session-selection'\n\n/** Canonical identities that have no direct Sandbox backend. */\nconst NON_BACKEND_HARNESSES = ['gemini'] as const satisfies readonly HarnessType[]\nconst nonBackendHarnesses = new Set<string>(NON_BACKEND_HARNESSES)\n\n/** A recognized coding-agent backend, not a promise that it is installed. */\nexport type Harness = Exclude<HarnessType, (typeof NON_BACKEND_HARNESSES)[number]>\n\n/**\n * Recognized backend identities, including IDs needed by historical sessions.\n * Use deployment availability, not membership here, to offer or admit new runs.\n */\nexport const KNOWN_HARNESSES: readonly Harness[] = [\n ...harnessTypeSchema.options.filter(\n (harness): harness is Exclude<HarnessType, (typeof NON_BACKEND_HARNESSES)[number]> =>\n !nonBackendHarnesses.has(harness),\n ),\n]\n\n/** Default for an absent choice; never a replacement for an invalid explicit ID. */\nexport const DEFAULT_HARNESS: Harness = 'opencode'\nconst HARNESS_SET: ReadonlySet<string> = new Set(KNOWN_HARNESSES)\n\n/** Determine whether a value is a recognized identity, independently of availability. */\nexport function isHarness(value: unknown): value is Harness {\n return typeof value === 'string' && HARNESS_SET.has(value)\n}\n\n/** Display/config convenience only. Never use coercion to admit an execution or restore a lock. */\nexport function coerceHarness(value: unknown, fallback: Harness = DEFAULT_HARNESS): Harness {\n return isHarness(value) ? value : fallback\n}\n\n/** Server-owned start and resume sets. Existing-only entries are draining. */\nexport type HarnessAvailability = SelectionAvailability<Harness>\n\n/** A saved lock, explicit choice, workspace default, and optional deployment availability. */\nexport type ResolveSessionHarnessInput = SessionHarnessSelectionInput<Harness>\n\n/** The selected identity and the existing session-lock/swap-attempt result. */\nexport type ResolvedSessionHarness = SessionHarnessSelection<Harness>\n\n/**\n * Resolve requested → workspaceDefault → fallback only for a genuinely new session.\n * A non-null saved identity always takes the lock path. Unknown/retired identities\n * and invalid explicit requests throw HarnessSelectionError instead of falling back.\n * A known different request still reports swapAttempted while preserving the lock.\n * The caller must reject that attempt before execution, as in the existing contract.\n */\nexport function resolveSessionHarness(input: ResolveSessionHarnessInput = {}): ResolvedSessionHarness {\n return selectSessionHarness(input, KNOWN_HARNESSES, DEFAULT_HARNESS)\n}\n\n/**\n * An app's harness configuration, read from its deploy configuration (for example the env vars\n * `AGENT_HARNESS` and `AGENT_HARNESS_WORKSPACES`), so no product hard-codes `backend.type`.\n */\nexport interface HarnessConfig {\n /** The app default for new sessions. Unset or blank means {@link DEFAULT_HARNESS}. */\n default?: string\n /** Per-workspace choices: a JSON object mapping a workspace id to a harness. */\n workspaces?: string\n}\n\nfunction configHarness(value: unknown, where: string): Harness {\n if (!isHarness(value)) throw new Error(`${where} names an unknown harness: ${String(value)}`)\n return value\n}\n\n/**\n * The harness a workspace's new sessions start on: its own choice, else the app default, else\n * {@link DEFAULT_HARNESS}. Pass the result to {@link resolveSessionHarness} as `workspaceDefault`\n * so a session that already ran keeps its harness. A value naming an unknown harness throws:\n * a configuration typo must fail loudly, never run a different harness than the one configured.\n */\nexport function configuredHarness(config: HarnessConfig, workspaceId?: string): Harness {\n const appDefault = config.default?.trim()\n ? configHarness(config.default.trim(), 'The harness default') : DEFAULT_HARNESS\n const raw = config.workspaces?.trim()\n if (!raw) return appDefault\n let choices: unknown\n try {\n choices = JSON.parse(raw)\n } catch {\n throw new Error('The per-workspace harness configuration is not JSON')\n }\n if (!choices || typeof choices !== 'object' || Array.isArray(choices)) {\n throw new Error('The per-workspace harness configuration must map workspace ids to harnesses')\n }\n // Every entry is checked, not only the caller's, so one bad entry fails every workspace's\n // first turn after the deploy instead of hiding until that workspace sends a message.\n const parsed = new Map(Object.entries(choices).map(([id, value]) =>\n [id, configHarness(value, `The harness for workspace ${id}`)]))\n return (workspaceId === undefined ? undefined : parsed.get(workspaceId)) ?? appDefault\n}\n\n/** Harness-model compatibility is delegated to the Interface owner. */\nexport { modelProvider }\n\n/** A resolved model and the transport provider that will execute it. */\nexport interface HarnessModelSelection {\n model: string\n provider?: string\n}\n\n/** Provider-less ids (sentinels like \"default\", or a session's own config) are\n * compatible everywhere — every harness honors its own configuration. */\nexport function isModelCompatibleWithHarness(harness: Harness, modelId: string): boolean {\n return harnessSupportsModel(harness, modelId)\n}\n\n/** Keep `modelId` when the harness can run it; else the harness's best compatible\n * catalog id (preferred patterns in order, highest version). When nothing in the\n * catalog fits, return the original so the caller sees the incompatibility. */\nexport function snapModelToHarness(harness: Harness, modelId: string, canonicalIds: readonly string[]): string {\n return aiSnapModelToHarness(harness, modelId, canonicalIds)\n}\n\n/** Keep the harness when it can run `modelId`; else the model's native harness\n * (anthropic → claude-code, openai → codex, moonshot → kimi-code), falling back to opencode. */\nexport function snapHarnessToModel(harness: Harness, modelId: string): Harness {\n const snapped = aiSnapHarnessToModel(harness, modelId)\n if (!isHarness(snapped)) {\n throw new Error(\n `Harness \"${harness}\" snapped to \"${snapped}\" for model \"${modelId}\", which this platform has no backend for.`,\n )\n }\n return snapped\n}\n\n/** Fail-loud server guard: throw when a harness is asked to run a model it can't.\n * A resolved model tuple supplies provider evidence when its id is unqualified.\n * Call before dispatching a sandbox turn so a bypassed UI cannot reach the\n * sidecar with an incompatible or unproven pair. */\nexport function assertHarnessModelCompatible(\n harness: Harness,\n selection: string | HarnessModelSelection,\n): void {\n const modelId = typeof selection === 'string' ? selection : selection.model\n const transportProvider = typeof selection === 'string' ? undefined : selection.provider\n const providers = harnessProviders(harness)\n const provider = modelProvider(modelId) ?? transportProvider ?? null\n // An omitted/default model is deliberately left to the harness. Once a\n // caller names a model, however, a vendor-locked harness must have provider\n // evidence. Treating a router provider as compatible with every native CLI\n // is how a bare Gemini model reached Claude Code and produced \"Not logged in\".\n const isDefaultSentinel = modelId.trim() === '' || modelId.trim() === 'default'\n const compatible = isDefaultSentinel\n || providers === null\n || (provider !== null && providers.includes(provider))\n if (!compatible) {\n const native = preferredHarnessForModel(modelId)\n const providerDescription = provider ?? 'unqualified'\n throw new Error(\n `Harness \"${harness}\" cannot run model \"${modelId}\" (provider \"${providerDescription}\"). ` +\n `Use ${native ?? 'a router-backed harness (opencode)'} or an allowed model.`,\n )\n }\n}\n\n// Profile-support facts remain here until the materializer supplies its owned descriptor.\n// Do not confuse these facts with deployment availability.\n\n/** What a harness can express, for profile assembly. */\nexport interface HarnessCapabilities {\n /** Lower profile.subagents into native cwd files; false means materialization refuses them. */\n subagents: boolean\n /** How profile.mcp reaches the harness. */\n mcp: 'native' | 'config-file' | 'extension' | 'none'\n /** Whether tool controls are checked before MCP tools register. */\n toolControls: 'startup' | 'runtime'\n /** cwd-relative skill directory, or null when skills must be inlined. */\n skillsDir: string | null\n}\n\nconst HARNESS_CAPABILITIES: Record<Harness, HarnessCapabilities> = {\n 'claude-code': { subagents: true, mcp: 'native', toolControls: 'runtime', skillsDir: '.claude/skills' },\n nanoclaw: { subagents: true, mcp: 'native', toolControls: 'runtime', skillsDir: '.claude/skills' },\n opencode: { subagents: true, mcp: 'config-file', toolControls: 'runtime', skillsDir: '.opencode/skills' },\n codex: { subagents: true, mcp: 'config-file', toolControls: 'runtime', skillsDir: '.codex/skills' },\n 'kimi-code': { subagents: false, mcp: 'config-file', toolControls: 'runtime', skillsDir: '.kimi/skills' },\n pi: { subagents: false, mcp: 'extension', toolControls: 'startup', skillsDir: '.pi/skills' },\n prime: { subagents: true, mcp: 'native', toolControls: 'runtime', skillsDir: '.prime/agent/skills' },\n hermes: { subagents: false, mcp: 'native', toolControls: 'runtime', skillsDir: null },\n openclaw: { subagents: false, mcp: 'config-file', toolControls: 'runtime', skillsDir: 'skills' },\n acp: { subagents: false, mcp: 'native', toolControls: 'runtime', skillsDir: null },\n amp: { subagents: false, mcp: 'native', toolControls: 'runtime', skillsDir: null },\n 'factory-droids': { subagents: false, mcp: 'native', toolControls: 'runtime', skillsDir: null },\n forge: { subagents: false, mcp: 'native', toolControls: 'runtime', skillsDir: null },\n cursor: { subagents: false, mcp: 'native', toolControls: 'runtime', skillsDir: null },\n 'cli-base': { subagents: false, mcp: 'native', toolControls: 'runtime', skillsDir: null },\n}\n\n/** Profile-support facts, independent of whether this deployment enables the harness. */\nexport function harnessCapabilities(harness: Harness): HarnessCapabilities {\n const caps = HARNESS_CAPABILITIES[harness]\n if (!caps) throw new Error(`harnessCapabilities: no capability facts for \"${harness}\"`)\n return caps\n}\n","/** Pure session admission. Harness identity is not deployment availability. */\nexport interface HarnessAvailability<H extends string> {\n /** Server-owned selection for new sessions. An empty list disables all starts. */\n readonly newSessions: readonly H[]\n /** Includes draining harnesses. Omission uses newSessions, never all known IDs. */\n readonly existingSessions?: readonly H[]\n}\n\nexport interface SessionHarnessSelectionInput<H extends string> {\n sessionHarness?: unknown\n requested?: unknown\n workspaceDefault?: unknown\n fallback?: H\n /** Omit only when the caller deliberately admits every known harness. */\n availability?: HarnessAvailability<H>\n}\n\nexport interface SessionHarnessSelection<H extends string> {\n harness: H\n locked: boolean\n swapAttempted: boolean\n}\n\ntype SelectionSource = 'session' | 'requested' | 'workspace' | 'fallback'\n\n/** An admission failure, never permission to retry on another harness. */\nexport class HarnessSelectionError extends Error {\n readonly code: 'harness_unknown' | 'harness_unavailable'\n readonly harness: string | null\n readonly source: SelectionSource\n\n constructor(\n code: 'harness_unknown' | 'harness_unavailable',\n value: unknown,\n source: SelectionSource,\n ) {\n const harness = typeof value === 'string' ? value : null\n super(`${code}: ${source} harness ${harness === null ? '<invalid>' : JSON.stringify(harness)}`)\n this.name = 'HarnessSelectionError'\n this.code = code\n this.harness = harness\n this.source = source\n }\n}\n\n/**\n * Null/undefined mean no choice. Every other value is an explicit choice and must\n * validate. A retired or unrecognized saved identity must never become a new session.\n * Availability is supplied by the authenticated server, not by a request body.\n */\nexport function selectSessionHarness<H extends string>(\n input: SessionHarnessSelectionInput<H>,\n knownHarnesses: readonly H[],\n defaultHarness: H,\n): SessionHarnessSelection<H> {\n const known = new Set<string>(knownHarnesses)\n const resolve = (value: unknown, source: SelectionSource): H => {\n if (typeof value !== 'string' || !known.has(value)) {\n throw new HarnessSelectionError('harness_unknown', value, source)\n }\n return value as H\n }\n const admit = (harness: H, source: SelectionSource, existing: boolean): void => {\n if (input.availability === undefined) return\n const allowed = existing\n ? input.availability.existingSessions ?? input.availability.newSessions\n : input.availability.newSessions\n if (!allowed.includes(harness)) {\n throw new HarnessSelectionError('harness_unavailable', harness, source)\n }\n }\n\n if (input.sessionHarness !== undefined && input.sessionHarness !== null) {\n const harness = resolve(input.sessionHarness, 'session')\n admit(harness, 'session', true)\n // A known different request remains a swap attempt; it is not executed. An\n // invalid request is refused rather than being misreported as no swap.\n const requested = input.requested === undefined || input.requested === null\n ? undefined : resolve(input.requested, 'requested')\n return { harness, locked: true, swapAttempted: requested !== undefined && requested !== harness }\n }\n\n const source: SelectionSource = input.requested !== undefined && input.requested !== null\n ? 'requested'\n : input.workspaceDefault !== undefined && input.workspaceDefault !== null\n ? 'workspace' : 'fallback'\n const value = source === 'requested' ? input.requested\n : source === 'workspace' ? input.workspaceDefault : input.fallback ?? defaultHarness\n const harness = resolve(value, source)\n admit(harness, source, false)\n return { harness, locked: false, swapAttempted: false }\n}\n"],"mappings":"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","names":["value","source","harness"]}
@@ -1,73 +1,44 @@
1
1
  /**
2
- * Coding-agent harness selection — taxonomy, coercion, and the session-lock invariant.
2
+ * Coding-agent harness selection, model compatibility, and the session lock.
3
3
  *
4
- * A "harness" is the coding-agent CLI a sandbox drives (opencode / codex /
5
- * claude-code / …). The shell governs WHICH harness a chat session uses and
6
- * enforces that a session is LOCKED to the harness it started with — the model
7
- * may change mid-session, the harness may not (swapping it mid-session would
8
- * orphan the session's running agent state). Every product otherwise hand-rolls
9
- * this and hard-codes a single harness; this is the one place the rule lives.
4
+ * Known identities are not a deployment's available runners. Products supply
5
+ * server-owned availability to resolveSessionHarness. Historical session IDs
6
+ * remain intact when their runner is unavailable; a harness change requires an
7
+ * explicit transfer, not a fallback or a rewritten session lock.
10
8
  *
11
- * Substrate-free: the harness list is derived from `@tangle-network/agent-interface`'s canonical
12
- * harness enum and mirrors the sandbox SDK's `BackendType` (no sandbox dependency). The consumer
13
- * owns storage — which harness a workspace defaults to, which one a session locked — and maps the
14
- * resolved value onto the SDK's `backend.type`.
15
- *
16
- * Harness↔model COMPATIBILITY (which models a harness can run, snapping) is NOT defined here — it
17
- * comes from `@tangle-network/agent-interface`, the single source of truth shared with the
18
- * sandbox-ui pickers and the cli-bridge backends. This module owns the harness TAXONOMY + the
19
- * session lock, and its server assertion also checks resolved transport-provider evidence.
9
+ * Model compatibility is owned by @tangle-network/agent-interface. The shell
10
+ * owns session selection; the consumer owns storage and deployment policy.
20
11
  */
21
12
  import { modelProvider, type HarnessType } from '@tangle-network/agent-interface';
22
- /**
23
- * Canonical harnesses that are NOT sandbox backends. Each value in `KNOWN_HARNESSES` is dispatched
24
- * as `backend.type`, so a harness the platform ships no provider adapter for must stay out —
25
- * offering it would resolve a session onto a runner that cannot start.
26
- */
13
+ import { type HarnessAvailability as SelectionAvailability, type SessionHarnessSelection, type SessionHarnessSelectionInput } from './session-selection';
14
+ export { HarnessSelectionError } from './session-selection';
15
+ /** Canonical identities that have no direct Sandbox backend. */
27
16
  declare const NON_BACKEND_HARNESSES: readonly ["gemini"];
28
- /** A coding-agent backend this shell can dispatch a session onto. */
17
+ /** A recognized coding-agent backend, not a promise that it is installed. */
29
18
  export type Harness = Exclude<HarnessType, (typeof NON_BACKEND_HARNESSES)[number]>;
30
19
  /**
31
- * The known coding-agent backends: the canonical harness set minus what has no provider adapter,
32
- * Derived from `harnessTypeSchema.options` so a harness added upstream reaches this shell without
33
- * an edit here, and kept structural so this module still needs no sandbox dependency.
20
+ * Recognized backend identities, including IDs needed by historical sessions.
21
+ * Use deployment availability, not membership here, to offer or admit new runs.
34
22
  */
35
23
  export declare const KNOWN_HARNESSES: readonly Harness[];
36
- /** Define the default harness to use for code execution and testing environments */
24
+ /** Default for an absent choice; never a replacement for an invalid explicit ID. */
37
25
  export declare const DEFAULT_HARNESS: Harness;
38
- /** Determine if a value is a recognized harness string identifier */
26
+ /** Determine whether a value is a recognized identity, independently of availability. */
39
27
  export declare function isHarness(value: unknown): value is Harness;
40
- /** Coerce an arbitrary value to a known harness, falling back (default `opencode`). */
28
+ /** Display/config convenience only. Never use coercion to admit an execution or restore a lock. */
41
29
  export declare function coerceHarness(value: unknown, fallback?: Harness): Harness;
42
- /** Resolve input options to determine the appropriate session harness to use */
43
- export interface ResolveSessionHarnessInput {
44
- /** The harness already locked to this session (recorded at its first turn). */
45
- sessionHarness?: unknown;
46
- /** The harness requested now — a new session's choice, or a turn's attempt to switch. */
47
- requested?: unknown;
48
- /** The workspace's default harness, used only when starting a fresh session. */
49
- workspaceDefault?: unknown;
50
- /** Final fallback when nothing else resolves (default `opencode`). */
51
- fallback?: Harness;
52
- }
53
- /** Represent resolved session state including harness, lock status, and swap attempt flag */
54
- export interface ResolvedSessionHarness {
55
- /** The harness to actually run — the locked one when the session already has it. */
56
- harness: Harness;
57
- /** True when the session already had a locked harness (this turn did not pick it). */
58
- locked: boolean;
59
- /** True when `requested` differs from the locked harness — a forbidden mid-session
60
- * swap the caller should reject or warn on. The lock always wins regardless. */
61
- swapAttempted: boolean;
62
- }
30
+ /** Server-owned start and resume sets. Existing-only entries are draining. */
31
+ export type HarnessAvailability = SelectionAvailability<Harness>;
32
+ /** A saved lock, explicit choice, workspace default, and optional deployment availability. */
33
+ export type ResolveSessionHarnessInput = SessionHarnessSelectionInput<Harness>;
34
+ /** The selected identity and the existing session-lock/swap-attempt result. */
35
+ export type ResolvedSessionHarness = SessionHarnessSelection<Harness>;
63
36
  /**
64
- * Resolve the harness for a turn, enforcing the session lock.
65
- *
66
- * - **Session already started** (`sessionHarness` is a known harness): that harness
67
- * wins (`locked: true`); a differing `requested` sets `swapAttempted` so the caller
68
- * can reject the swap. The model is a separate per-turn concern and is unaffected.
69
- * - **Fresh session**: pick `requested → workspaceDefault → fallback`. The caller
70
- * persists the result as the session's lock for every subsequent turn.
37
+ * Resolve requested → workspaceDefault → fallback only for a genuinely new session.
38
+ * A non-null saved identity always takes the lock path. Unknown/retired identities
39
+ * and invalid explicit requests throw HarnessSelectionError instead of falling back.
40
+ * A known different request still reports swapAttempted while preserving the lock.
41
+ * The caller must reject that attempt before execution, as in the existing contract.
71
42
  */
72
43
  export declare function resolveSessionHarness(input?: ResolveSessionHarnessInput): ResolvedSessionHarness;
73
44
  /**
@@ -87,13 +58,7 @@ export interface HarnessConfig {
87
58
  * a configuration typo must fail loudly, never run a different harness than the one configured.
88
59
  */
89
60
  export declare function configuredHarness(config: HarnessConfig, workspaceId?: string): Harness;
90
- /**
91
- * Harness ↔ model compatibility + snapping — delegated to `@tangle-network/agent-interface`.
92
- *
93
- * `Harness` is the dispatchable subset of `HarnessType`: this shell drops only canonical harnesses
94
- * for which the platform has no backend. Compatibility therefore comes entirely from Interface;
95
- * this package keeps no second harness taxonomy.
96
- */
61
+ /** Harness-model compatibility is delegated to the Interface owner. */
97
62
  export { modelProvider };
98
63
  /** A resolved model and the transport provider that will execute it. */
99
64
  export interface HarnessModelSelection {
@@ -117,20 +82,14 @@ export declare function snapHarnessToModel(harness: Harness, modelId: string): H
117
82
  export declare function assertHarnessModelCompatible(harness: Harness, selection: string | HarnessModelSelection): void;
118
83
  /** What a harness can express, for profile assembly. */
119
84
  export interface HarnessCapabilities {
120
- /** Lower `profile.subagents` into native cwd files (e.g. `.claude/agents/`).
121
- * False: a profile carrying subagents is refused at materialization —
122
- * builders must not add them. */
85
+ /** Lower profile.subagents into native cwd files; false means materialization refuses them. */
123
86
  subagents: boolean;
124
- /** How `profile.mcp` reaches the harness. */
87
+ /** How profile.mcp reaches the harness. */
125
88
  mcp: 'native' | 'config-file' | 'extension' | 'none';
126
- /** When tool controls are validated: 'startup' means an allowlist naming
127
- * MCP-carried verbs is checked before MCP tools register (spawn-time
128
- * validation refuses unknown names); 'runtime' tolerates them. */
89
+ /** Whether tool controls are checked before MCP tools register. */
129
90
  toolControls: 'startup' | 'runtime';
130
- /** cwd-relative directory `resources.skills` materializes into, or null
131
- * when the harness has no cwd skill primitive (falls back to inline). */
91
+ /** cwd-relative skill directory, or null when skills must be inlined. */
132
92
  skillsDir: string | null;
133
93
  }
134
- /** The capability facts for `harness`. Every harness in KNOWN_HARNESSES has
135
- * an entry; the map is exhaustive by construction (tests assert parity). */
94
+ /** Profile-support facts, independent of whether this deployment enables the harness. */
136
95
  export declare function harnessCapabilities(harness: Harness): HarnessCapabilities;
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  DEFAULT_HARNESS,
3
+ HarnessSelectionError,
3
4
  KNOWN_HARNESSES,
4
5
  assertHarnessModelCompatible,
5
6
  coerceHarness,
@@ -11,9 +12,10 @@ import {
11
12
  resolveSessionHarness,
12
13
  snapHarnessToModel,
13
14
  snapModelToHarness
14
- } from "../chunk-MXNVSY6J.js";
15
+ } from "../chunk-VBRUG4N5.js";
15
16
  export {
16
17
  DEFAULT_HARNESS,
18
+ HarnessSelectionError,
17
19
  KNOWN_HARNESSES,
18
20
  assertHarnessModelCompatible,
19
21
  coerceHarness,
@@ -0,0 +1,35 @@
1
+ /** Pure session admission. Harness identity is not deployment availability. */
2
+ export interface HarnessAvailability<H extends string> {
3
+ /** Server-owned selection for new sessions. An empty list disables all starts. */
4
+ readonly newSessions: readonly H[];
5
+ /** Includes draining harnesses. Omission uses newSessions, never all known IDs. */
6
+ readonly existingSessions?: readonly H[];
7
+ }
8
+ export interface SessionHarnessSelectionInput<H extends string> {
9
+ sessionHarness?: unknown;
10
+ requested?: unknown;
11
+ workspaceDefault?: unknown;
12
+ fallback?: H;
13
+ /** Omit only when the caller deliberately admits every known harness. */
14
+ availability?: HarnessAvailability<H>;
15
+ }
16
+ export interface SessionHarnessSelection<H extends string> {
17
+ harness: H;
18
+ locked: boolean;
19
+ swapAttempted: boolean;
20
+ }
21
+ type SelectionSource = 'session' | 'requested' | 'workspace' | 'fallback';
22
+ /** An admission failure, never permission to retry on another harness. */
23
+ export declare class HarnessSelectionError extends Error {
24
+ readonly code: 'harness_unknown' | 'harness_unavailable';
25
+ readonly harness: string | null;
26
+ readonly source: SelectionSource;
27
+ constructor(code: 'harness_unknown' | 'harness_unavailable', value: unknown, source: SelectionSource);
28
+ }
29
+ /**
30
+ * Null/undefined mean no choice. Every other value is an explicit choice and must
31
+ * validate. A retired or unrecognized saved identity must never become a new session.
32
+ * Availability is supplied by the authenticated server, not by a request body.
33
+ */
34
+ export declare function selectSessionHarness<H extends string>(input: SessionHarnessSelectionInput<H>, knownHarnesses: readonly H[], defaultHarness: H): SessionHarnessSelection<H>;
35
+ export {};
@@ -3,4 +3,4 @@ export { createHubIntegrationsClient, type HubIntegrationsClient, type HubIntegr
3
3
  export { connectionTone, HubIntegrationsPanel, hubConnectionStatus, type HubAccountStatus, type HubConnectionState, type HubIntegrationsAccounts, type HubIntegrationsPanelProps } from './panel';
4
4
  export { HUB_CONNECT_CHANNEL, HUB_CONNECTED_MESSAGE_TYPE, HUB_CONNECT_FAILED_MESSAGE_TYPE, POPUP_TIMEOUT_MS } from './popup';
5
5
  export { useHubIntegrations, type HubHostConnect, type HubWriteReceipt, type UseHubIntegrationsOptions } from './use-hub-integrations';
6
- export { type HubIntegrationCapabilities, type HubIntegrationCapability } from './projection';
6
+ export { type HubActionPolicy, type HubActionPolicyDisplay, type HubIntegrationCapabilities, type HubIntegrationCapability } from './projection';
@@ -102,7 +102,7 @@ function waitForConnection(input) {
102
102
  candidate.onmessage = (event) => {
103
103
  const message = event.data;
104
104
  if (!message || message.provider !== input.providerId || message.nonce !== input.nonce || message.context !== input.contextToken) return;
105
- if (message.type === HUB_CONNECT_FAILED_MESSAGE_TYPE) finish("cancelled");
105
+ if (message.type === HUB_CONNECT_FAILED_MESSAGE_TYPE) finish("failed");
106
106
  if (message.type === HUB_CONNECTED_MESSAGE_TYPE) {
107
107
  if (pollId !== void 0) window.clearTimeout(pollId);
108
108
  void check();
@@ -187,7 +187,7 @@ function HubConnectCallbackPage({ returnHref }) {
187
187
  setPhase("failed");
188
188
  return;
189
189
  }
190
- const failure = params.get("error_description") || params.get("error");
190
+ const failure = params.get("error_description") || params.get("error") || (params.get("hub_connection") === "error" ? params.get("hub_error") || "Please restart the connection flow." : null);
191
191
  try {
192
192
  const channel = new BroadcastChannel(HUB_CONNECT_CHANNEL);
193
193
  channel.postMessage({ type: failure ? HUB_CONNECT_FAILED_MESSAGE_TYPE : HUB_CONNECTED_MESSAGE_TYPE, provider, nonce, context });
@@ -352,7 +352,7 @@ var OPTIONS = [
352
352
  { value: "ask", label: "Ask" },
353
353
  { value: "deny", label: "Deny" }
354
354
  ];
355
- function permissionDetails(tools, policies, connectionId, can) {
355
+ function permissionDetails(tools, policies, connectionId, can, getActionPolicy) {
356
356
  const overrides = new Map(policies.filter((policy) => policy.connectionId === connectionId).map((policy) => [policy.actionPath, policy.decision]));
357
357
  const groups = [
358
358
  { id: "writes", title: "Write and unknown actions", rows: [] },
@@ -362,16 +362,17 @@ function permissionDetails(tools, policies, connectionId, can) {
362
362
  const reads = [];
363
363
  for (const tool of tools) {
364
364
  const override = overrides.get(tool.path);
365
+ const enforced = getActionPolicy?.(tool, override);
365
366
  const row = {
366
367
  actionPath: tool.path,
367
368
  title: tool.title ?? tool.path,
368
369
  riskLabel: tool.risk ?? "unknown",
369
370
  riskTone: tool.risk === "destructive" ? "error" : tool.risk === "write" || tool.risk === void 0 ? "warning" : "neutral",
370
- decision: override ?? null,
371
- decisionOptions: OPTIONS,
372
- sourceLabel: override ? "Stored override" : "No stored override",
373
- canReset: override !== void 0 && can({ operation: "policy.reset", connectionId, actionPath: tool.path }),
374
- disabled: !can({ operation: "policy.set", connectionId, actionPath: tool.path })
371
+ decision: enforced?.decision ?? override ?? null,
372
+ decisionOptions: enforced ? [{ value: enforced.decision, label: enforced.label }] : OPTIONS,
373
+ sourceLabel: enforced?.sourceLabel ?? (override ? "Stored override" : "No stored override"),
374
+ canReset: !enforced && override !== void 0 && can({ operation: "policy.reset", connectionId, actionPath: tool.path }),
375
+ disabled: !!enforced || !can({ operation: "policy.set", connectionId, actionPath: tool.path })
375
376
  };
376
377
  if (tool.risk === "read") reads.push(row);
377
378
  else writes.push(row);
@@ -399,7 +400,7 @@ function updateUrl(providerId, connectionId) {
399
400
  else url.searchParams.delete("connection");
400
401
  window.history.pushState(window.history.state, "", url);
401
402
  }
402
- function useHubIntegrations({ identity, client, can, callbackPath, onUnsupportedConnect, hostConnect }) {
403
+ function useHubIntegrations({ identity, client, can, callbackPath, onUnsupportedConnect, hostConnect, getActionPolicy }) {
403
404
  const scope = contextKey(identity);
404
405
  const activeScope = useRef(scope);
405
406
  activeScope.current = scope;
@@ -544,7 +545,7 @@ function useHubIntegrations({ identity, client, can, callbackPath, onUnsupported
544
545
  connections.retry();
545
546
  setDialog(null);
546
547
  setConnectStatus({ scope, status: "connected", message: "Connection verified." });
547
- } else setConnectStatus({ scope, status: "failed", message: result === "blocked" ? "Allow the popup and try again." : result === "unverified" ? "Could not verify a new connection. Refresh and try again." : result === "cancelled" ? "Connection was cancelled." : "This connection flow is unavailable." });
548
+ } else setConnectStatus({ scope, status: "failed", providerId: target.providerId, message: result === "blocked" ? "Allow the popup and try again." : result === "unverified" ? "Could not verify a new connection. Refresh and try again." : result === "failed" ? "Could not connect your account. Please try again." : result === "cancelled" ? "Connection was cancelled." : "This connection flow is unavailable." });
548
549
  return result;
549
550
  }, [can, connections, client, identity, callbackPath, scope]);
550
551
  const beginConnect = useCallback((target) => {
@@ -580,7 +581,7 @@ function useHubIntegrations({ identity, client, can, callbackPath, onUnsupported
580
581
  pendingPopup.current?.abort();
581
582
  pendingPopup.current = null;
582
583
  setDialog(null);
583
- setConnectStatus({ scope, status: "failed", message: "Connection was cancelled." });
584
+ setConnectStatus((current) => ({ scope, status: "failed", providerId: current.scope === scope ? current.providerId : void 0, message: "Connection was cancelled." }));
584
585
  }, [scope]);
585
586
  const rows = useMemo(
586
587
  () => catalogRows(
@@ -593,7 +594,7 @@ function useHubIntegrations({ identity, client, can, callbackPath, onUnsupported
593
594
  ),
594
595
  [providerList, connectionList, visibleSelection.providerId, visibleSelection.connectionId, can, onUnsupportedConnect, hostConnect]
595
596
  );
596
- const details = detail.status === "ready" && connection ? permissionDetails(detail.value.tools, detail.value.policies, connection.id, can) : void 0;
597
+ const details = detail.status === "ready" && connection ? permissionDetails(detail.value.tools, detail.value.policies, connection.id, can, (tool, storedDecision) => getActionPolicy?.({ connection, tool, storedDecision })) : void 0;
597
598
  return {
598
599
  providers,
599
600
  connections,
@@ -795,6 +796,7 @@ function HubIntegrationsPanel(props) {
795
796
  }, [hub.connectStatus.status, scope]);
796
797
  const connectedProviders = new Set(connectionList.filter((connection) => connection.status !== "revoked").map((connection) => connection.providerId));
797
798
  const catalogRows2 = props.accounts && hub.connections.status === "error" ? [] : props.accounts ? rows.filter((row) => !connectedProviders.has(row.providerId)).map((row) => ({ ...row, connections: [], selectedConnectionId: null })) : rows;
799
+ const retryProvider = hub.providers.status === "ready" ? hub.providers.value.find((provider) => provider.providerId === hub.connectStatus.providerId) : void 0;
798
800
  const selectedConnection = hub.connection;
799
801
  const detailError = hub.detail.status === "error" ? hub.detail.message : hub.detail.status === "ready" && hub.detail.value.truncated ? `Showing at most ${hub.detail.value.tools.length} actions. The provider catalog may be incomplete.` : null;
800
802
  const detailsByConnectionId = useMemo2(() => hub.connection ? {
@@ -841,7 +843,6 @@ function HubIntegrationsPanel(props) {
841
843
  loading: hub.providers.status === "loading" || hub.connections.status === "loading",
842
844
  error,
843
845
  actionError: mutationError,
844
- connectError: hub.connectStatus.status === "failed" ? hub.connectStatus.message : null,
845
846
  busyProviderId: hub.connectStatus.status === "pending" ? hub.connectStatus.providerId ?? null : null,
846
847
  reserveConnectionContext: true,
847
848
  emptyCatalogLabel: props.accounts && rows.length > 0 ? "Every available integration has a connected account. Manage an account to connect another." : void 0
@@ -855,7 +856,10 @@ function HubIntegrationsPanel(props) {
855
856
  hub.writeState.status === "pending" ? /* @__PURE__ */ jsx2("p", { role: "status", children: "Saving Hub settings\u2026" }) : null,
856
857
  mutationError ? /* @__PURE__ */ jsx2("p", { role: "alert", children: mutationError }) : null,
857
858
  writeMessage ? /* @__PURE__ */ jsx2("p", { role: writeWarning ? "alert" : "status", children: writeMessage }) : null,
858
- hub.connectStatus.message && hub.provider ? /* @__PURE__ */ jsx2("p", { role: hub.connectStatus.status === "failed" ? "alert" : "status", children: hub.connectStatus.message }) : null,
859
+ hub.connectStatus.status === "failed" ? /* @__PURE__ */ jsxs2("div", { role: "alert", className: "mb-4 flex flex-wrap items-center gap-3 text-sm text-destructive", children: [
860
+ /* @__PURE__ */ jsx2("p", { "data-testid": "integration-connect-error", children: hub.connectStatus.message }),
861
+ retryProvider ? /* @__PURE__ */ jsx2(Button, { type: "button", variant: "outline", disabled: pending, onClick: () => hub.beginConnect(retryProvider), children: "Retry connection" }) : null
862
+ ] }) : hub.connectStatus.message && hub.provider ? /* @__PURE__ */ jsx2("p", { role: "status", children: hub.connectStatus.message }) : null,
859
863
  hub.provider ? /* @__PURE__ */ jsxs2(Fragment, { children: [
860
864
  props.accounts && selectedConnection || selectedRow?.canConnect ? /* @__PURE__ */ jsxs2("div", { className: "mb-4 flex min-w-0 flex-wrap items-center gap-2", children: [
861
865
  props.accounts && selectedConnection ? /* @__PURE__ */ jsxs2(