diff --git a/crates/ironclaw_reborn_composition/src/slack_host_beta/runtime_setup.rs b/crates/ironclaw_reborn_composition/src/slack_host_beta/runtime_setup.rs
index ae2e7cfd83b..4e003eda91b 100644
--- a/crates/ironclaw_reborn_composition/src/slack_host_beta/runtime_setup.rs
+++ b/crates/ironclaw_reborn_composition/src/slack_host_beta/runtime_setup.rs
@@ -554,25 +554,22 @@ impl DynamicSlackTriggeredRunDeliveryHook {
#[async_trait::async_trait]
impl PostSubmitDeliveryHook for DynamicSlackTriggeredRunDeliveryHook {
async fn on_trigger_submitted(&self, fire: TriggerFire, run_id: TurnRunId, scope: TurnScope) {
- let hook = self.clone();
- tokio::spawn(async move {
- match hook.current_driver().await {
- Ok(Some(driver)) => driver.on_trigger_submitted(fire, run_id, scope).await,
- Ok(None) => {
- tracing::debug!(
- %run_id,
- "Slack dynamic triggered-run delivery skipped: Slack setup is not configured"
- );
- }
- Err(error) => {
- tracing::warn!(
- %run_id,
- %error,
- "Slack dynamic triggered-run delivery skipped: delivery hook unavailable"
- );
- }
+ match self.current_driver().await {
+ Ok(Some(driver)) => driver.on_trigger_submitted(fire, run_id, scope).await,
+ Ok(None) => {
+ tracing::debug!(
+ %run_id,
+ "Slack dynamic triggered-run delivery skipped: Slack setup is not configured"
+ );
}
- });
+ Err(error) => {
+ tracing::warn!(
+ %run_id,
+ %error,
+ "Slack dynamic triggered-run delivery skipped: delivery hook unavailable"
+ );
+ }
+ }
}
}
diff --git a/crates/ironclaw_webui_v2_static/src/assets.rs b/crates/ironclaw_webui_v2_static/src/assets.rs
index 6cd91f37bc6..634041c02dc 100644
--- a/crates/ironclaw_webui_v2_static/src/assets.rs
+++ b/crates/ironclaw_webui_v2_static/src/assets.rs
@@ -258,16 +258,11 @@ mod tests {
}
#[test]
- fn chat_connect_action_assets_render_slack_pairing_and_extensions_channel_picker() {
+ fn chat_omits_connect_action_while_extensions_render_slack_setup_ui() {
let chat = asset_text("js/pages/chat/chat.js");
- assert!(chat.contains("ChannelConnectCard"));
- assert!(chat.contains("channelConnectAction"));
- assert!(chat.contains("dismissChannelConnectAction"));
-
- let card = asset_text("js/pages/chat/components/channel-connect-card.js");
- assert!(card.contains("SlackPairingSection"));
- assert!(card.contains("isSlackStrategy(connectAction, \"inbound_proof_code\")"));
- assert!(card.contains("action=${connectAction.action}"));
+ assert!(!chat.contains("ChannelConnectCard"));
+ assert!(!chat.contains("channelConnectAction"));
+ assert!(!chat.contains("dismissChannelConnectAction"));
let picker = asset_text("js/components/slack-channel-picker.js");
assert!(picker.contains("listSlackAllowedChannels"));
@@ -288,9 +283,10 @@ mod tests {
assert!(channels_tab.contains("action=${action.action}"));
let regression = source_text("js/pages/chat/lib/useChat-send.test.mjs");
- assert!(regression.contains("channel connect requests return an action"));
- assert!(regression.contains("without submitting a prompt"));
- assert!(regression.contains("unmatched channel connect requests submit the prompt"));
+ assert!(regression.contains(
+ "slash connect text submits to the model without fetching connectable channels"
+ ));
+ assert!(regression.contains("ordinary Slack chat prompts submit to the model"));
}
#[test]
diff --git a/crates/ironclaw_webui_v2_static/static/js/lib/channel-connect.js b/crates/ironclaw_webui_v2_static/static/js/lib/channel-connect.js
index 003865e1ff2..6665e3eecb6 100644
--- a/crates/ironclaw_webui_v2_static/static/js/lib/channel-connect.js
+++ b/crates/ironclaw_webui_v2_static/static/js/lib/channel-connect.js
@@ -3,79 +3,3 @@ import { apiFetch } from "./api.js";
export function listConnectableChannels() {
return apiFetch("/api/webchat/v2/channels/connectable");
}
-
-export function resolveChannelConnectCommand(input, channels) {
- if (!looksLikeChannelConnectCommand(input)) return null;
- const text = normalizedWords(input);
- const requireExplicitCommandAlias = explicitlyTargetsSlackChannelManagement(text);
- let best = null;
- for (const channel of channels || []) {
- if (!isChatCommandResolvable(channel)) continue;
- const matchLength = bestMatchingAliasLength(text, channel, {
- commandAliasesOnly: requireExplicitCommandAlias,
- });
- if (matchLength > (best?.matchLength || 0)) {
- best = { channel, matchLength };
- }
- }
- return best?.channel || null;
-}
-
-export function looksLikeChannelConnectCommand(input) {
- const text = normalizedWords(input);
- if (!text) return false;
- const intent = /(^|\s)(connect|link|pair|setup|set up)(\s|$)/.test(text);
- const target = /(^|\s)(account|channel|app|integration|slack|telegram|whatsapp)(\s|$)/.test(text);
- return intent && target;
-}
-
-function channelAliases(channel) {
- return [
- channel?.channel,
- channel?.display_name,
- ...(Array.isArray(channel?.command_aliases) ? channel.command_aliases : []),
- ].filter(Boolean);
-}
-
-function commandAliases(channel, options = {}) {
- const aliases = Array.isArray(channel?.command_aliases)
- ? channel.command_aliases.filter(Boolean)
- : [];
- if (!options.channelManagementOnly) return aliases;
- return aliases.filter((alias) => explicitlyTargetsChannelManagement(normalizedWords(alias)));
-}
-
-function isChatCommandResolvable(channel) {
- return channel?.strategy !== "admin_managed_channels";
-}
-
-function explicitlyTargetsSlackChannelManagement(text) {
- return includesWordPhrase(text, "slack") && explicitlyTargetsChannelManagement(text);
-}
-
-function explicitlyTargetsChannelManagement(text) {
- return /(^|\s)(channel|channels|allowlist)(\s|$)/.test(text);
-}
-
-function normalizedWords(value) {
- return String(value || "")
- .toLowerCase()
- .replace(/[^a-z0-9]+/g, " ")
- .trim()
- .replace(/\s+/g, " ");
-}
-
-function bestMatchingAliasLength(text, channel, options = {}) {
- const aliases = options.commandAliasesOnly
- ? commandAliases(channel, { channelManagementOnly: true })
- : channelAliases(channel);
- return aliases.reduce((best, alias) => {
- const phrase = normalizedWords(alias);
- return includesWordPhrase(text, phrase) ? Math.max(best, phrase.length) : best;
- }, 0);
-}
-
-function includesWordPhrase(text, phrase) {
- if (!phrase) return false;
- return ` ${text} `.includes(` ${phrase} `);
-}
diff --git a/crates/ironclaw_webui_v2_static/static/js/lib/channel-connect.test.mjs b/crates/ironclaw_webui_v2_static/static/js/lib/channel-connect.test.mjs
deleted file mode 100644
index 32602a65e53..00000000000
--- a/crates/ironclaw_webui_v2_static/static/js/lib/channel-connect.test.mjs
+++ /dev/null
@@ -1,63 +0,0 @@
-import assert from "node:assert/strict";
-import test from "node:test";
-
-import { resolveChannelConnectCommand } from "./channel-connect.js";
-
-const slack = {
- channel: "slack",
- display_name: "Slack",
- command_aliases: ["slack", "slack account"],
-};
-
-const telegram = {
- channel: "telegram",
- display_name: "Telegram",
- command_aliases: ["telegram"],
-};
-
-test("resolveChannelConnectCommand detects explicit Slack connect requests", () => {
- assert.equal(resolveChannelConnectCommand("connect my Slack account", [slack]), slack);
- assert.equal(resolveChannelConnectCommand("pair slack", [slack]), slack);
- assert.equal(resolveChannelConnectCommand("link the slack app", [slack]), slack);
-});
-
-test("resolveChannelConnectCommand leaves admin-managed Slack channel commands out of chat", () => {
- const personalSlack = {
- channel: "slack",
- display_name: "Slack",
- strategy: "inbound_proof_code",
- command_aliases: ["slack", "slack account", "slack pairing"],
- };
- const adminSlack = {
- channel: "slack",
- display_name: "Slack",
- strategy: "admin_managed_channels",
- command_aliases: [],
- };
-
- assert.equal(
- resolveChannelConnectCommand("connect slack", [personalSlack, adminSlack]),
- personalSlack,
- );
- assert.equal(
- resolveChannelConnectCommand("connect slack channel", [personalSlack, adminSlack]),
- null,
- );
- assert.equal(
- resolveChannelConnectCommand("connect slack allowlist", [
- personalSlack,
- { ...adminSlack, command_aliases: ["slack allowlist"] },
- ]),
- null,
- );
-});
-
-test("resolveChannelConnectCommand only suppresses Slack channel management wording", () => {
- assert.equal(resolveChannelConnectCommand("connect telegram channel", [telegram]), telegram);
-});
-
-test("resolveChannelConnectCommand leaves ordinary Slack prompts for the model", () => {
- assert.equal(resolveChannelConnectCommand("send a message to Slack", [slack]), null);
- assert.equal(resolveChannelConnectCommand("what is slack?", [slack]), null);
- assert.equal(resolveChannelConnectCommand("connect the two ideas", [slack]), null);
-});
diff --git a/crates/ironclaw_webui_v2_static/static/js/pages/chat/chat.js b/crates/ironclaw_webui_v2_static/static/js/pages/chat/chat.js
index 1a7fea6bbaa..a3535f951db 100644
--- a/crates/ironclaw_webui_v2_static/static/js/pages/chat/chat.js
+++ b/crates/ironclaw_webui_v2_static/static/js/pages/chat/chat.js
@@ -11,7 +11,6 @@ import { ApprovalCard } from "./components/approval-card.js";
import { AuthGenericCard } from "./components/auth-generic-card.js";
import { AuthOauthCard } from "./components/auth-oauth-card.js";
import { AuthTokenCard } from "./components/auth-token-card.js";
-import { ChannelConnectCard } from "./components/channel-connect-card.js";
import { ChatInput } from "./components/chat-input.js";
import { ConnectionStatus } from "./components/connection-status.js";
import { EmptyState } from "./components/empty-state.js";
@@ -53,7 +52,6 @@ export function Chat({
isProcessing,
pendingGate,
busyGateNotice,
- channelConnectAction,
suggestions,
sseStatus,
historyLoading,
@@ -70,7 +68,6 @@ export function Chat({
loadMore,
setSuggestions,
submitAuthToken,
- dismissChannelConnectAction,
} = useChat(activeThreadId);
const activeThread = React.useMemo(
@@ -86,8 +83,7 @@ export function Chat({
const hasMessages =
messages.length > 0 ||
activeThreadIsProcessing ||
- activeThreadHasGate ||
- Boolean(channelConnectAction);
+ activeThreadHasGate;
// Don't show the landing composer when history failed to load — show the
// error banner instead so the user is not misled into thinking the thread
// is empty.
@@ -293,13 +289,6 @@ export function Chat({
/>
`}
${activeThreadIsProcessing && !activeThreadHasGate && html`<${TypingIndicator} />`}
- ${channelConnectAction &&
- html`
- <${ChannelConnectCard}
- connectAction=${channelConnectAction}
- onDismiss=${dismissChannelConnectAction}
- />
- `}
${pendingGate &&
(pendingGate.kind === "auth_required"
? (pendingGate.challengeKind === "oauth_url"
diff --git a/crates/ironclaw_webui_v2_static/static/js/pages/chat/components/channel-connect-card.js b/crates/ironclaw_webui_v2_static/static/js/pages/chat/components/channel-connect-card.js
deleted file mode 100644
index b0b85266518..00000000000
--- a/crates/ironclaw_webui_v2_static/static/js/pages/chat/components/channel-connect-card.js
+++ /dev/null
@@ -1,58 +0,0 @@
-import { ChannelPairingSection } from "../../../components/channel-pairing-section.js";
-import { SlackPairingSection } from "../../../components/slack-pairing-section.js";
-import { Icon } from "../../../design-system/icons.js";
-import { html } from "../../../lib/html.js";
-
-export function isSlackStrategy(connectAction, strategy) {
- return connectAction?.channel === "slack" && connectAction.strategy === strategy;
-}
-
-export function ChannelConnectCard({ connectAction, onDismiss }) {
- if (!connectAction) return null;
- const channel = connectAction.channel;
-
- return html`
-
-
-
-
- Connect ${connectAction.display_name || channel}
-
-
- ${onDismiss &&
- html`
-
- `}
-
-
- ${isSlackStrategy(connectAction, "inbound_proof_code")
- ? html`<${SlackPairingSection} action=${connectAction.action} />`
- : connectAction.strategy === "inbound_proof_code"
- ? html`
- <${ChannelPairingSection}
- channel=${channel}
- action=${connectAction.action}
- />
- `
- : html`
-
- ${connectAction.action?.instructions ||
- "This channel exposes a connect action, but the WebUI has no renderer for its strategy yet."}
-
- `}
-
- `;
-}
diff --git a/crates/ironclaw_webui_v2_static/static/js/pages/chat/components/channel-connect-card.test.mjs b/crates/ironclaw_webui_v2_static/static/js/pages/chat/components/channel-connect-card.test.mjs
deleted file mode 100644
index db690bcd67c..00000000000
--- a/crates/ironclaw_webui_v2_static/static/js/pages/chat/components/channel-connect-card.test.mjs
+++ /dev/null
@@ -1,42 +0,0 @@
-import assert from "node:assert/strict";
-import { readFileSync } from "node:fs";
-import test from "node:test";
-import vm from "node:vm";
-
-function channelConnectCardSourceForTest() {
- const source = readFileSync(new URL("./channel-connect-card.js", import.meta.url), "utf8");
- const lines = [];
- for (const line of source.split("\n")) {
- if (line.startsWith("import ")) continue;
- lines.push(line.replace(/^export function /, "function "));
- }
- return `${lines.join("\n")}\nglobalThis.__testExports = { isSlackStrategy };`;
-}
-
-test("isSlackStrategy gates the Slack personal pairing renderer", () => {
- const context = { globalThis: {} };
- vm.runInNewContext(channelConnectCardSourceForTest(), context);
- const { isSlackStrategy } = context.globalThis.__testExports;
-
- assert.equal(
- isSlackStrategy(
- { channel: "slack", strategy: "inbound_proof_code" },
- "inbound_proof_code",
- ),
- true,
- );
- assert.equal(
- isSlackStrategy(
- { channel: "slack", strategy: "inbound_proof_code" },
- "admin_managed_channels",
- ),
- false,
- );
- assert.equal(
- isSlackStrategy(
- { channel: "teams", strategy: "inbound_proof_code" },
- "inbound_proof_code",
- ),
- false,
- );
-});
diff --git a/crates/ironclaw_webui_v2_static/static/js/pages/chat/hooks/useChat.js b/crates/ironclaw_webui_v2_static/static/js/pages/chat/hooks/useChat.js
index 6259a605c70..e9238f7a437 100644
--- a/crates/ironclaw_webui_v2_static/static/js/pages/chat/hooks/useChat.js
+++ b/crates/ironclaw_webui_v2_static/static/js/pages/chat/hooks/useChat.js
@@ -5,11 +5,6 @@ import {
sendMessage,
submitManualToken,
} from "../../../lib/api.js";
-import {
- listConnectableChannels,
- looksLikeChannelConnectCommand,
- resolveChannelConnectCommand,
-} from "../../../lib/channel-connect.js";
import { queryClient } from "../../../lib/query-client.js";
import { React } from "../../../lib/html.js";
import { useChatEvents } from "../lib/useChatEvents.js";
@@ -117,21 +112,6 @@ function parseOAuthCallbackStoragePayload(value) {
}
}
-async function resolveConnectAction(content) {
- if (!looksLikeChannelConnectCommand(content)) return null;
- try {
- const channelsResponse = await queryClient.fetchQuery({
- queryKey: ["connectable-channels"],
- queryFn: listConnectableChannels,
- });
- const channels = channelsResponse?.channels || [];
- return resolveChannelConnectCommand(content, channels);
- } catch (err) {
- console.error("Failed to resolve connectable channels:", err);
- return null;
- }
-}
-
// v2 chat hook. Differences from the fork's v1 hook:
// - No image / attachment plumbing — v2 SendMessage carries `content` only.
// - No /api/chat/approval — approvals fold into gate/resolve in v2.
@@ -160,8 +140,6 @@ export function useChat(threadId) {
React.useEffect(() => {
activeRunRef.current = activeRun;
}, [activeRun]);
- const [channelConnectAction, setChannelConnectAction] = React.useState(null);
-
const getPendingMessages = React.useCallback(
() => pendingMessagesRef.current.get(threadId || "__new__") || [],
[threadId],
@@ -252,7 +230,6 @@ export function useChat(threadId) {
setPendingGateState(null);
setBusyGateNotice(null);
setActiveRunState(null);
- setChannelConnectAction(null);
}
React.useEffect(() => {
@@ -459,18 +436,6 @@ export function useChat(threadId) {
return null;
}
- // Channel-connect slash commands ("/connect telegram") never carry
- // attachments; skip that detection when files are staged so an
- // upload is never misread as a command and dropped.
- if (stagedAttachments.length === 0) {
- const connectable = await resolveConnectAction(content);
- if (connectable) {
- setChannelConnectAction(connectable);
- return { channel_connect_action: connectable };
- }
- }
- setChannelConnectAction(null);
-
let sendThreadId = targetThreadId || threadId;
if (!sendThreadId) {
@@ -952,7 +917,6 @@ export function useChat(threadId) {
isProcessing,
pendingGate,
busyGateNotice,
- channelConnectAction,
activeRun,
sseStatus,
historyLoading,
@@ -964,7 +928,6 @@ export function useChat(threadId) {
submitAuthToken,
cancelRun,
loadMore,
- dismissChannelConnectAction: () => setChannelConnectAction(null),
// fork-shape compatibility — see comments above
suggestions: [],
setSuggestions: noop,
diff --git a/crates/ironclaw_webui_v2_static/static/js/pages/chat/lib/chat.test.mjs b/crates/ironclaw_webui_v2_static/static/js/pages/chat/lib/chat.test.mjs
index 55127b0121a..e2843d5f91f 100644
--- a/crates/ironclaw_webui_v2_static/static/js/pages/chat/lib/chat.test.mjs
+++ b/crates/ironclaw_webui_v2_static/static/js/pages/chat/lib/chat.test.mjs
@@ -152,7 +152,6 @@ test("Chat leaves the composer editable while a run is processing", () => {
messages: [{ id: "message-1" }],
isProcessing: true,
pendingGate: null,
- channelConnectAction: null,
suggestions: [],
sseStatus: "open",
historyLoading: false,
@@ -168,7 +167,6 @@ test("Chat leaves the composer editable while a run is processing", () => {
loadMore: () => {},
setSuggestions: () => {},
submitAuthToken: async () => {},
- dismissChannelConnectAction: () => {},
},
});
@@ -185,7 +183,6 @@ test("Chat refuses composer sends while a run is processing", async () => {
messages: [{ id: "message-1" }],
isProcessing: true,
pendingGate: null,
- channelConnectAction: null,
suggestions: [],
sseStatus: "open",
historyLoading: false,
@@ -204,7 +201,6 @@ test("Chat refuses composer sends while a run is processing", async () => {
loadMore: () => {},
setSuggestions: () => {},
submitAuthToken: async () => {},
- dismissChannelConnectAction: () => {},
},
});
@@ -303,7 +299,6 @@ test("Chat keeps the new-conversation composer sendable while a prior run is set
messages: [],
isProcessing: true,
pendingGate: null,
- channelConnectAction: null,
suggestions: [],
sseStatus: "open",
historyLoading: false,
@@ -322,7 +317,6 @@ test("Chat keeps the new-conversation composer sendable while a prior run is set
loadMore: () => {},
setSuggestions: () => {},
submitAuthToken: async () => {},
- dismissChannelConnectAction: () => {},
},
});
@@ -346,7 +340,6 @@ test("Chat renders a timeline load failure as an alert instead of the empty land
messages: [],
isProcessing: false,
pendingGate: null,
- channelConnectAction: null,
suggestions: [],
sseStatus: "open",
historyLoading: false,
@@ -363,7 +356,6 @@ test("Chat renders a timeline load failure as an alert instead of the empty land
loadMore: () => {},
setSuggestions: () => {},
submitAuthToken: async () => {},
- dismissChannelConnectAction: () => {},
},
});
@@ -381,7 +373,6 @@ test("Chat links to scoped logs for the active thread run", () => {
messages: [{ id: "message-1" }],
isProcessing: true,
pendingGate: null,
- channelConnectAction: null,
suggestions: [],
sseStatus: "open",
historyLoading: false,
@@ -397,7 +388,6 @@ test("Chat links to scoped logs for the active thread run", () => {
loadMore: () => {},
setSuggestions: () => {},
submitAuthToken: async () => {},
- dismissChannelConnectAction: () => {},
},
});
diff --git a/crates/ironclaw_webui_v2_static/static/js/pages/chat/lib/useChat-send.test.mjs b/crates/ironclaw_webui_v2_static/static/js/pages/chat/lib/useChat-send.test.mjs
index 7fa68cd5203..c41093b4665 100644
--- a/crates/ironclaw_webui_v2_static/static/js/pages/chat/lib/useChat-send.test.mjs
+++ b/crates/ironclaw_webui_v2_static/static/js/pages/chat/lib/useChat-send.test.mjs
@@ -5,10 +5,6 @@ import vm from "node:vm";
import { messagesFromTimeline } from "./history-messages.js";
import { toRenderAttachment, toWireAttachment } from "./attachments.js";
-import {
- looksLikeChannelConnectCommand,
- resolveChannelConnectCommand,
-} from "../../../lib/channel-connect.js";
import {
addPending,
recordAcceptedMessageRef,
@@ -21,6 +17,20 @@ import {
resetToolActivityState,
} from "./tool-activity-state.js";
+const STATE_SLOT = Object.freeze({
+ cooldownUntil: 0,
+ now: 1,
+ activeRun: 2,
+ isProcessing: 3,
+ pendingGate: 4,
+ busyGateNotice: 5,
+ stateThreadId: 6,
+});
+
+function stateUpdatesFor(updates, slot) {
+ return updates.filter((update) => update.index === slot);
+}
+
function useChatSourceForTest() {
const source = readFileSync(
new URL("../hooks/useChat.js", import.meta.url),
@@ -138,10 +148,6 @@ test("useChat.send: accepted ref reconciles pending message on timeline reload",
throw new Error("thread should already exist");
},
globalThis: {},
- listConnectableChannels: async () => {
- throw new Error("ordinary prompts should not fetch connectable channels");
- },
- looksLikeChannelConnectCommand,
queryClient: {
fetchQuery: async () => {
throw new Error("ordinary prompts should not fetch connectable channels");
@@ -151,7 +157,6 @@ test("useChat.send: accepted ref reconciles pending message on timeline reload",
recordAcceptedMessageRef,
removePending,
timelineMessageIdFromAcceptedRef,
- resolveChannelConnectCommand,
resolveGateRequest: async () => {},
sendMessage: async () => ({
accepted_message_ref: "msg:message-1",
@@ -235,10 +240,6 @@ function createSendCaptureContext() {
throw new Error("thread should already exist");
},
globalThis: {},
- listConnectableChannels: async () => {
- throw new Error("attachment sends should not fetch connectable channels");
- },
- looksLikeChannelConnectCommand,
queryClient: {
fetchQuery: async () => {
throw new Error("attachment sends should not fetch connectable channels");
@@ -248,7 +249,6 @@ function createSendCaptureContext() {
recordAcceptedMessageRef: () => null,
removePending,
timelineMessageIdFromAcceptedRef,
- resolveChannelConnectCommand,
resolveGateRequest: async () => {},
sendMessage: async (body) => {
sentBody = body;
@@ -376,10 +376,6 @@ test("useChat.send: target-thread send does not append into active thread", asyn
throw new Error("target thread should already exist");
},
globalThis: {},
- listConnectableChannels: async () => {
- throw new Error("ordinary prompts should not fetch connectable channels");
- },
- looksLikeChannelConnectCommand,
queryClient: {
fetchQuery: async () => {
throw new Error("ordinary prompts should not fetch connectable channels");
@@ -388,7 +384,6 @@ test("useChat.send: target-thread send does not append into active thread", asyn
},
recordAcceptedMessageRef,
removePending,
- resolveChannelConnectCommand,
resolveGateRequest: async () => {},
sendMessage: async ({ threadId }) => ({
accepted_message_ref: "msg:target-message-1",
@@ -431,8 +426,8 @@ test("useChat.send: target-thread send does not append into active thread", asyn
seededByThread.get(targetThreadId)[0].timelineMessageId,
"target-message-1",
);
- assert.deepEqual(stateUpdates.filter((update) => update.index === 2), []);
- assert.deepEqual(stateUpdates.filter((update) => update.index === 4), []);
+ assert.deepEqual(stateUpdatesFor(stateUpdates, STATE_SLOT.activeRun), []);
+ assert.deepEqual(stateUpdatesFor(stateUpdates, STATE_SLOT.isProcessing), []);
});
test("useChat.send: target-thread rejected_busy updates seeded cache", async () => {
@@ -458,10 +453,6 @@ test("useChat.send: target-thread rejected_busy updates seeded cache", async ()
throw new Error("target thread should already exist");
},
globalThis: {},
- listConnectableChannels: async () => {
- throw new Error("ordinary prompts should not fetch connectable channels");
- },
- looksLikeChannelConnectCommand,
queryClient: {
fetchQuery: async () => {
throw new Error("ordinary prompts should not fetch connectable channels");
@@ -471,7 +462,6 @@ test("useChat.send: target-thread rejected_busy updates seeded cache", async ()
recordAcceptedMessageRef,
removePending,
timelineMessageIdFromAcceptedRef,
- resolveChannelConnectCommand,
resolveGateRequest: async () => {},
sendMessage: async () => ({
outcome: "rejected_busy",
@@ -513,8 +503,8 @@ test("useChat.send: target-thread rejected_busy updates seeded cache", async ()
assert.equal(targetMessages[0].status, "error");
assert.equal(targetMessages[1].role, "system");
assert.equal(targetMessages[1].content, "Thread is busy, please try again.");
- assert.deepEqual(stateUpdates.filter((update) => update.index === 2), []);
- assert.deepEqual(stateUpdates.filter((update) => update.index === 4), []);
+ assert.deepEqual(stateUpdatesFor(stateUpdates, STATE_SLOT.activeRun), []);
+ assert.deepEqual(stateUpdatesFor(stateUpdates, STATE_SLOT.isProcessing), []);
});
test("useChat.send: target-thread thrown errors update seeded cache", async () => {
@@ -540,10 +530,6 @@ test("useChat.send: target-thread thrown errors update seeded cache", async () =
throw new Error("target thread should already exist");
},
globalThis: {},
- listConnectableChannels: async () => {
- throw new Error("ordinary prompts should not fetch connectable channels");
- },
- looksLikeChannelConnectCommand,
queryClient: {
fetchQuery: async () => {
throw new Error("ordinary prompts should not fetch connectable channels");
@@ -553,7 +539,6 @@ test("useChat.send: target-thread thrown errors update seeded cache", async () =
recordAcceptedMessageRef,
removePending,
timelineMessageIdFromAcceptedRef,
- resolveChannelConnectCommand,
resolveGateRequest: async () => {},
sendMessage: async () => {
throw new Error("network unavailable");
@@ -596,8 +581,8 @@ test("useChat.send: target-thread thrown errors update seeded cache", async () =
assert.equal(targetMessages[0].isOptimistic, false);
assert.equal(targetMessages[0].status, "error");
assert.equal(targetMessages[0].error, "network unavailable");
- assert.deepEqual(stateUpdates.filter((update) => update.index === 2), []);
- assert.deepEqual(stateUpdates.filter((update) => update.index === 4), []);
+ assert.deepEqual(stateUpdatesFor(stateUpdates, STATE_SLOT.activeRun), []);
+ assert.deepEqual(stateUpdatesFor(stateUpdates, STATE_SLOT.isProcessing), []);
});
test("useChat.send: pending approval blocks before sendMessage", async () => {
@@ -620,8 +605,8 @@ test("useChat.send: pending approval blocks before sendMessage", async () => {
Math,
React: createReactStub({
initialByIndex: new Map([
- [4, false],
- [5, pendingGate],
+ [STATE_SLOT.isProcessing, false],
+ [STATE_SLOT.pendingGate, pendingGate],
]),
setCalls: stateUpdates,
}),
@@ -634,10 +619,6 @@ test("useChat.send: pending approval blocks before sendMessage", async () => {
throw new Error("thread should already exist");
},
globalThis: {},
- listConnectableChannels: async () => {
- throw new Error("ordinary prompts should not fetch connectable channels");
- },
- looksLikeChannelConnectCommand,
queryClient: {
fetchQuery: async () => {
throw new Error("ordinary prompts should not fetch connectable channels");
@@ -646,7 +627,6 @@ test("useChat.send: pending approval blocks before sendMessage", async () => {
},
recordAcceptedMessageRef,
removePending,
- resolveChannelConnectCommand,
resolveGateRequest: async () => {},
sendMessage: async () => {
sendCalls += 1;
@@ -713,8 +693,8 @@ test("useChat.retryMessage: pre-admission rejection keeps failed bubble retryabl
Math,
React: createReactStub({
initialByIndex: new Map([
- [4, false],
- [5, pendingGate],
+ [STATE_SLOT.isProcessing, false],
+ [STATE_SLOT.pendingGate, pendingGate],
]),
}),
addPending,
@@ -726,10 +706,6 @@ test("useChat.retryMessage: pre-admission rejection keeps failed bubble retryabl
throw new Error("thread should already exist");
},
globalThis: {},
- listConnectableChannels: async () => {
- throw new Error("approval gate should block before channel discovery");
- },
- looksLikeChannelConnectCommand,
queryClient: {
fetchQuery: async () => {
throw new Error("approval gate should block before channel discovery");
@@ -738,7 +714,6 @@ test("useChat.retryMessage: pre-admission rejection keeps failed bubble retryabl
},
recordAcceptedMessageRef,
removePending,
- resolveChannelConnectCommand,
resolveGateRequest: async () => {},
sendMessage: async () => {
sendCalls += 1;
@@ -806,8 +781,8 @@ test("useChat.send: accepted send does not clear a gate received while in flight
Math,
React: createReactStub({
initialByIndex: new Map([
- [4, false],
- [5, null],
+ [STATE_SLOT.isProcessing, false],
+ [STATE_SLOT.pendingGate, null],
]),
setCalls: stateUpdates,
stateSlots,
@@ -821,10 +796,6 @@ test("useChat.send: accepted send does not clear a gate received while in flight
throw new Error("thread should already exist");
},
globalThis: {},
- listConnectableChannels: async () => {
- throw new Error("ordinary prompts should not fetch connectable channels");
- },
- looksLikeChannelConnectCommand,
queryClient: {
fetchQuery: async () => {
throw new Error("ordinary prompts should not fetch connectable channels");
@@ -834,7 +805,6 @@ test("useChat.send: accepted send does not clear a gate received while in flight
recordAcceptedMessageRef,
removePending,
timelineMessageIdFromAcceptedRef,
- resolveChannelConnectCommand,
resolveGateRequest: async () => {},
sendMessage: async () => {
setPendingGateFromEvents(replacementGate);
@@ -873,11 +843,11 @@ test("useChat.send: accepted send does not clear a gate received while in flight
await chat.send("accepted after gate changed");
assert.deepEqual(
- stateUpdates.filter((call) => call.index === 5).map((call) => call.value),
+ stateUpdatesFor(stateUpdates, STATE_SLOT.pendingGate).map((call) => call.value),
[replacementGate],
"non-busy success must not clear a gate received while send was in flight",
);
- assert.equal(stateSlots.get(5).value, replacementGate);
+ assert.equal(stateSlots.get(STATE_SLOT.pendingGate).value, replacementGate);
});
test("useChat.send: rejected busy attaches notice to a gate received while in flight", async () => {
@@ -901,8 +871,8 @@ test("useChat.send: rejected busy attaches notice to a gate received while in fl
Math,
React: createReactStub({
initialByIndex: new Map([
- [4, false],
- [5, null],
+ [STATE_SLOT.isProcessing, false],
+ [STATE_SLOT.pendingGate, null],
]),
setCalls: stateUpdates,
stateSlots,
@@ -916,10 +886,6 @@ test("useChat.send: rejected busy attaches notice to a gate received while in fl
throw new Error("thread should already exist");
},
globalThis: {},
- listConnectableChannels: async () => {
- throw new Error("ordinary prompts should not fetch connectable channels");
- },
- looksLikeChannelConnectCommand,
queryClient: {
fetchQuery: async () => {
throw new Error("ordinary prompts should not fetch connectable channels");
@@ -929,7 +895,6 @@ test("useChat.send: rejected busy attaches notice to a gate received while in fl
recordAcceptedMessageRef,
removePending,
timelineMessageIdFromAcceptedRef,
- resolveChannelConnectCommand,
resolveGateRequest: async () => {},
sendMessage: async () => {
setPendingGateFromEvents(replacementGate);
@@ -968,14 +933,15 @@ test("useChat.send: rejected busy attaches notice to a gate received while in fl
await chat.send("busy after gate changed");
assert.deepEqual(
- stateUpdates.filter((call) => call.index === 5).map((call) => call.value),
+ stateUpdatesFor(stateUpdates, STATE_SLOT.pendingGate).map((call) => call.value),
[replacementGate],
"busy rejection must leave a concurrently received gate untouched",
);
- assert.equal(stateSlots.get(5).value, replacementGate);
- const busyNoticeUpdates = stateUpdates
- .filter((call) => call.index === 6)
- .map((call) => call.value);
+ assert.equal(stateSlots.get(STATE_SLOT.pendingGate).value, replacementGate);
+ const busyNoticeUpdates = stateUpdatesFor(
+ stateUpdates,
+ STATE_SLOT.busyGateNotice,
+ ).map((call) => call.value);
assert.equal(busyNoticeUpdates.length, 1);
assert.equal(busyNoticeUpdates[0].content, "Thread is busy, please try again.");
assert.match(busyNoticeUpdates[0].gateKey, /run-replacement\ngate-replacement$/);
@@ -1005,8 +971,8 @@ test("useChat.send: rejected busy seeds notice when active thread changed in fli
Math,
React: createReactStub({
initialByIndex: new Map([
- [4, false],
- [5, null],
+ [STATE_SLOT.isProcessing, false],
+ [STATE_SLOT.pendingGate, null],
]),
setCalls: stateUpdates,
stateSlots,
@@ -1021,10 +987,6 @@ test("useChat.send: rejected busy seeds notice when active thread changed in fli
throw new Error("thread should already exist");
},
globalThis: {},
- listConnectableChannels: async () => {
- throw new Error("ordinary prompts should not fetch connectable channels");
- },
- looksLikeChannelConnectCommand,
queryClient: {
fetchQuery: async () => {
throw new Error("ordinary prompts should not fetch connectable channels");
@@ -1034,7 +996,6 @@ test("useChat.send: rejected busy seeds notice when active thread changed in fli
recordAcceptedMessageRef,
removePending,
timelineMessageIdFromAcceptedRef,
- resolveChannelConnectCommand,
resolveGateRequest: async () => {},
sendMessage: async () => {
refs[0].current = nextThreadId;
@@ -1078,7 +1039,7 @@ test("useChat.send: rejected busy seeds notice when active thread changed in fli
await chat.send("busy after thread switch");
assert.deepEqual(
- stateUpdates.filter((call) => call.index === 6).map((call) => call.value),
+ stateUpdatesFor(stateUpdates, STATE_SLOT.stateThreadId).map((call) => call.value),
[],
"a busy gate notice must not be written into a thread that became active later",
);
@@ -1106,8 +1067,8 @@ test("useChat.send: rejected busy appends system notice after gate resolves in f
Math,
React: createReactStub({
initialByIndex: new Map([
- [4, false],
- [5, null],
+ [STATE_SLOT.isProcessing, false],
+ [STATE_SLOT.pendingGate, null],
]),
setCalls: stateUpdates,
stateSlots,
@@ -1121,10 +1082,6 @@ test("useChat.send: rejected busy appends system notice after gate resolves in f
throw new Error("thread should already exist");
},
globalThis: {},
- listConnectableChannels: async () => {
- throw new Error("ordinary prompts should not fetch connectable channels");
- },
- looksLikeChannelConnectCommand,
queryClient: {
fetchQuery: async () => {
throw new Error("ordinary prompts should not fetch connectable channels");
@@ -1134,7 +1091,6 @@ test("useChat.send: rejected busy appends system notice after gate resolves in f
recordAcceptedMessageRef,
removePending,
timelineMessageIdFromAcceptedRef,
- resolveChannelConnectCommand,
resolveGateRequest: async () => {},
sendMessage: async () => {
setPendingGateFromEvents(null);
@@ -1172,13 +1128,13 @@ test("useChat.send: rejected busy appends system notice after gate resolves in f
const chat = context.globalThis.__testExports.useChat(threadId);
await chat.send("busy after gate resolved");
- assert.equal(stateSlots.get(5).value, null);
+ assert.equal(stateSlots.get(STATE_SLOT.busyGateNotice).value, null);
assert.equal(renderedMessages.length, 2);
assert.equal(renderedMessages[0].status, "error");
assert.equal(renderedMessages[1].role, "system");
assert.equal(renderedMessages[1].content, "Thread is busy, please try again.");
assert.deepEqual(
- stateUpdates.filter((call) => call.index === 6).map((call) => call.value),
+ stateUpdatesFor(stateUpdates, STATE_SLOT.stateThreadId).map((call) => call.value),
[],
"a resolved gate should not get a lingering card-level busy notice",
);
@@ -1205,8 +1161,8 @@ test("useChat.send: gate received after callback creation blocks before send", a
Math,
React: createReactStub({
initialByIndex: new Map([
- [4, false],
- [5, null],
+ [STATE_SLOT.isProcessing, false],
+ [STATE_SLOT.pendingGate, null],
]),
setCalls: stateUpdates,
stateSlots,
@@ -1220,10 +1176,6 @@ test("useChat.send: gate received after callback creation blocks before send", a
throw new Error("thread should already exist");
},
globalThis: {},
- listConnectableChannels: async () => {
- throw new Error("ordinary prompts should not fetch connectable channels");
- },
- looksLikeChannelConnectCommand,
queryClient: {
fetchQuery: async () => {
throw new Error("ordinary prompts should not fetch connectable channels");
@@ -1233,7 +1185,6 @@ test("useChat.send: gate received after callback creation blocks before send", a
recordAcceptedMessageRef,
removePending,
timelineMessageIdFromAcceptedRef,
- resolveChannelConnectCommand,
resolveGateRequest: async () => {},
sendMessage: async () => {
throw new Error("sendMessage should not run");
@@ -1272,14 +1223,14 @@ test("useChat.send: gate received after callback creation blocks before send", a
);
assert.deepEqual(
- stateUpdates.filter((call) => call.index === 5).map((call) => call.value),
+ stateUpdatesFor(stateUpdates, STATE_SLOT.pendingGate).map((call) => call.value),
[pendingGate],
"send must read the latest gate from SSE instead of a stale null callback closure",
);
- assert.equal(stateSlots.get(5).value, pendingGate);
+ assert.equal(stateSlots.get(STATE_SLOT.pendingGate).value, pendingGate);
assert.equal(renderedMessages.length, 0);
assert.deepEqual(
- stateUpdates.filter((call) => call.index === 6).map((call) => call.value?.content),
+ stateUpdatesFor(stateUpdates, STATE_SLOT.busyGateNotice).map((call) => call.value?.content),
[],
);
});
@@ -1304,8 +1255,8 @@ test("useChat.send: repeated sends under the same pending gate stay blocked loca
Math,
React: createReactStub({
initialByIndex: new Map([
- [4, false],
- [5, pendingGate],
+ [STATE_SLOT.isProcessing, false],
+ [STATE_SLOT.pendingGate, pendingGate],
]),
setCalls: stateUpdates,
}),
@@ -1318,10 +1269,6 @@ test("useChat.send: repeated sends under the same pending gate stay blocked loca
throw new Error("thread should already exist");
},
globalThis: {},
- listConnectableChannels: async () => {
- throw new Error("ordinary prompts should not fetch connectable channels");
- },
- looksLikeChannelConnectCommand,
queryClient: {
fetchQuery: async () => {
throw new Error("ordinary prompts should not fetch connectable channels");
@@ -1330,7 +1277,6 @@ test("useChat.send: repeated sends under the same pending gate stay blocked loca
},
recordAcceptedMessageRef,
removePending,
- resolveChannelConnectCommand,
resolveGateRequest: async () => {},
sendMessage: async () => {
sendCalls += 1;
@@ -1386,11 +1332,11 @@ test("useChat.cancelRun clears local state before cancel request resolves", asyn
Math,
React: createReactStub({
// useChat state call order: cooldownUntil, now, activeRun,
- // channelConnectAction, isProcessing, pendingGate.
+ // isProcessing, pendingGate.
initialByIndex: new Map([
- [2, { runId: "run-1", threadId, status: "running" }],
- [4, true],
- [5, { runId: "run-1", gateRef: "gate-1" }],
+ [STATE_SLOT.activeRun, { runId: "run-1", threadId, status: "running" }],
+ [STATE_SLOT.isProcessing, true],
+ [STATE_SLOT.pendingGate, { runId: "run-1", gateRef: "gate-1" }],
]),
setCalls: stateUpdates,
}),
@@ -1408,17 +1354,12 @@ test("useChat.cancelRun clears local state before cancel request resolves", asyn
throw new Error("createThread should not run");
},
globalThis: {},
- listConnectableChannels: async () => ({
- channels: [],
- }),
- looksLikeChannelConnectCommand,
queryClient: {
fetchQuery: async () => ({ channels: [] }),
invalidateQueries: () => {},
},
recordAcceptedMessageRef,
removePending,
- resolveChannelConnectCommand,
resolveGateRequest: async () => {},
sendMessage: async () => {
throw new Error("sendMessage should not run");
@@ -1448,8 +1389,8 @@ test("useChat.cancelRun clears local state before cancel request resolves", asyn
assert.equal(cancelRequest.runId, "run-1");
assert.equal(cancelRequest.reason, "user_requested");
assert.deepEqual(stateUpdates.slice(0, 3), [
- { index: 5, value: null },
- { index: 4, value: false },
+ { index: 4, value: null },
+ { index: 3, value: false },
{ index: 2, value: null },
]);
@@ -1467,15 +1408,14 @@ test("useChat clears transient run and gate state during thread switch render",
Math,
React: createReactStub({
// useChat state call order: cooldownUntil, now, activeRun,
- // channelConnectAction, isProcessing, pendingGate, busyGateNotice,
+ // isProcessing, pendingGate, busyGateNotice,
// stateThreadId.
initialByIndex: new Map([
- [2, { runId: "run-old", threadId: "thread-old", status: "awaiting_gate" }],
- [3, { channel: "slack" }],
- [4, true],
- [5, { runId: "run-old", gateRef: "gate-old" }],
- [6, { gateKey: "thread-old\nrun-old\ngate-old", content: "busy" }],
- [7, "thread-old"],
+ [STATE_SLOT.activeRun, { runId: "run-old", threadId: "thread-old", status: "awaiting_gate" }],
+ [STATE_SLOT.isProcessing, true],
+ [STATE_SLOT.pendingGate, { runId: "run-old", gateRef: "gate-old" }],
+ [STATE_SLOT.busyGateNotice, { gateKey: "thread-old\nrun-old\ngate-old", content: "busy" }],
+ [STATE_SLOT.stateThreadId, "thread-old"],
]),
setCalls: stateUpdates,
}),
@@ -1488,15 +1428,12 @@ test("useChat clears transient run and gate state during thread switch render",
throw new Error("createThread should not run");
},
globalThis: {},
- listConnectableChannels: async () => ({ channels: [] }),
- looksLikeChannelConnectCommand,
queryClient: {
fetchQuery: async () => ({ channels: [] }),
invalidateQueries: () => {},
},
recordAcceptedMessageRef,
removePending,
- resolveChannelConnectCommand,
resolveGateRequest: async () => {},
sendMessage: async () => {
throw new Error("sendMessage should not run");
@@ -1521,12 +1458,11 @@ test("useChat clears transient run and gate state during thread switch render",
context.globalThis.__testExports.useChat("thread-new");
assert.deepEqual(stateUpdates.slice(0, 6), [
- { index: 7, value: "thread-new" },
- { index: 4, value: false },
+ { index: 6, value: "thread-new" },
+ { index: 3, value: false },
+ { index: 4, value: null },
{ index: 5, value: null },
- { index: 6, value: null },
{ index: 2, value: null },
- { index: 3, value: null },
]);
});
@@ -1554,9 +1490,9 @@ test("useChat.approve deny marks the current gated tool declined before resume",
Math,
React: createReactStub({
initialByIndex: new Map([
- [2, { runId, threadId, status: "awaiting_gate" }],
- [4, false],
- [5, {
+ [STATE_SLOT.activeRun, { runId, threadId, status: "awaiting_gate" }],
+ [STATE_SLOT.isProcessing, false],
+ [STATE_SLOT.pendingGate, {
runId,
gateRef,
kind: "gate",
@@ -1575,15 +1511,12 @@ test("useChat.approve deny marks the current gated tool declined before resume",
createToolActivityState,
failGateToolActivity,
globalThis: {},
- listConnectableChannels: async () => ({ channels: [] }),
- looksLikeChannelConnectCommand,
queryClient: {
fetchQuery: async () => ({ channels: [] }),
invalidateQueries: () => {},
},
recordAcceptedMessageRef,
removePending,
- resolveChannelConnectCommand,
resolveGateRequest: async (request) => {
resolveRequest = request;
return { outcome: "resumed", run_id: runId, status: "queued" };
@@ -1628,8 +1561,8 @@ test("useChat.approve deny marks the current gated tool declined before resume",
assert.equal(renderedMessages[0].toolErrorKind, "gate_declined");
assert.equal(renderedMessages[0].gateRef, gateRef);
assert.deepEqual(JSON.parse(JSON.stringify(stateUpdates.slice(-3))), [
- { index: 5, value: null },
- { index: 4, value: true },
+ { index: 4, value: null },
+ { index: 3, value: true },
{ index: 2, value: { runId, threadId, status: "queued" } },
]);
});
@@ -1648,9 +1581,9 @@ test("useChat.approve deny treats queued response without outcome as resumed", a
Math,
React: createReactStub({
initialByIndex: new Map([
- [2, { runId, threadId, status: "awaiting_gate" }],
- [4, false],
- [5, {
+ [STATE_SLOT.activeRun, { runId, threadId, status: "awaiting_gate" }],
+ [STATE_SLOT.isProcessing, false],
+ [STATE_SLOT.pendingGate, {
runId,
gateRef,
kind: "gate",
@@ -1669,15 +1602,12 @@ test("useChat.approve deny treats queued response without outcome as resumed", a
createToolActivityState,
failGateToolActivity,
globalThis: {},
- listConnectableChannels: async () => ({ channels: [] }),
- looksLikeChannelConnectCommand,
queryClient: {
fetchQuery: async () => ({ channels: [] }),
invalidateQueries: () => {},
},
recordAcceptedMessageRef,
removePending,
- resolveChannelConnectCommand,
resolveGateRequest: async () => ({ run_id: runId, status: "queued" }),
resetToolActivityState,
sendMessage: async () => {
@@ -1705,8 +1635,8 @@ test("useChat.approve deny treats queued response without outcome as resumed", a
await chat.approve(null, "deny", "gate");
assert.deepEqual(JSON.parse(JSON.stringify(stateUpdates.slice(-3))), [
- { index: 5, value: null },
- { index: 4, value: true },
+ { index: 4, value: null },
+ { index: 3, value: true },
{ index: 2, value: { runId, threadId, status: "queued" } },
]);
});
@@ -1725,9 +1655,9 @@ test("useChat.approve treats already_terminal false as resumed", async () => {
Math,
React: createReactStub({
initialByIndex: new Map([
- [2, { runId, threadId, status: "awaiting_gate" }],
- [4, false],
- [5, {
+ [STATE_SLOT.activeRun, { runId, threadId, status: "awaiting_gate" }],
+ [STATE_SLOT.isProcessing, false],
+ [STATE_SLOT.pendingGate, {
runId,
gateRef,
kind: "gate",
@@ -1746,15 +1676,12 @@ test("useChat.approve treats already_terminal false as resumed", async () => {
createToolActivityState,
failGateToolActivity,
globalThis: {},
- listConnectableChannels: async () => ({ channels: [] }),
- looksLikeChannelConnectCommand,
queryClient: {
fetchQuery: async () => ({ channels: [] }),
invalidateQueries: () => {},
},
recordAcceptedMessageRef,
removePending,
- resolveChannelConnectCommand,
resolveGateRequest: async () => ({ run_id: runId, already_terminal: false }),
resetToolActivityState,
sendMessage: async () => {
@@ -1782,8 +1709,8 @@ test("useChat.approve treats already_terminal false as resumed", async () => {
await chat.approve(null, "deny", "gate");
assert.deepEqual(JSON.parse(JSON.stringify(stateUpdates.slice(-3))), [
- { index: 5, value: null },
- { index: 4, value: true },
+ { index: 4, value: null },
+ { index: 3, value: true },
{ index: 2, value: { runId, threadId, status: "queued" } },
]);
});
@@ -1812,9 +1739,9 @@ test("useChat.approve deny with already_terminal true does not synthesize failed
Math,
React: createReactStub({
initialByIndex: new Map([
- [2, { runId, threadId, status: "awaiting_gate" }],
- [4, false],
- [5, {
+ [STATE_SLOT.activeRun, { runId, threadId, status: "awaiting_gate" }],
+ [STATE_SLOT.isProcessing, false],
+ [STATE_SLOT.pendingGate, {
runId,
gateRef,
kind: "gate",
@@ -1833,15 +1760,12 @@ test("useChat.approve deny with already_terminal true does not synthesize failed
createToolActivityState,
failGateToolActivity,
globalThis: {},
- listConnectableChannels: async () => ({ channels: [] }),
- looksLikeChannelConnectCommand,
queryClient: {
fetchQuery: async () => ({ channels: [] }),
invalidateQueries: () => {},
},
recordAcceptedMessageRef,
removePending,
- resolveChannelConnectCommand,
resolveGateRequest: async () => ({ run_id: runId, already_terminal: true }),
resetToolActivityState,
sendMessage: async () => {
@@ -1874,12 +1798,14 @@ test("useChat.approve deny with already_terminal true does not synthesize failed
assert.equal(renderedMessages[0].toolStatus, "ok");
assert.equal(renderedMessages[0].toolError, undefined);
assert.deepEqual(JSON.parse(JSON.stringify(stateUpdates.slice(-3))), [
- { index: 5, value: null },
- { index: 4, value: false },
- { index: 2, value: null },
+ { index: STATE_SLOT.pendingGate, value: null },
+ { index: STATE_SLOT.isProcessing, value: false },
+ { index: STATE_SLOT.activeRun, value: null },
]);
assert.equal(
- stateUpdates.some((update) => update.index === 4 && update.value === true),
+ stateUpdates.some(
+ (update) => update.index === STATE_SLOT.isProcessing && update.value === true,
+ ),
false,
"already_terminal gate resolution must not turn processing back on",
);
@@ -1898,8 +1824,8 @@ test("useChat.cancelRun completion does not clear a newer run", async () => {
Math,
React: createReactStub({
initialByIndex: new Map([
- [2, { runId: "run-1", threadId, status: "running" }],
- [4, true],
+ [STATE_SLOT.activeRun, { runId: "run-1", threadId, status: "running" }],
+ [STATE_SLOT.isProcessing, true],
]),
setCalls: stateUpdates,
}),
@@ -1915,10 +1841,6 @@ test("useChat.cancelRun completion does not clear a newer run", async () => {
throw new Error("createThread should not run");
},
globalThis: {},
- listConnectableChannels: async () => {
- throw new Error("ordinary prompts should not fetch connectable channels");
- },
- looksLikeChannelConnectCommand,
queryClient: {
fetchQuery: async () => {
throw new Error("ordinary prompts should not fetch connectable channels");
@@ -1927,7 +1849,6 @@ test("useChat.cancelRun completion does not clear a newer run", async () => {
},
recordAcceptedMessageRef,
removePending,
- resolveChannelConnectCommand,
resolveGateRequest: async () => {},
sendMessage: async () => ({
accepted_message_ref: "msg:message-2",
@@ -1958,7 +1879,8 @@ test("useChat.cancelRun completion does not clear a newer run", async () => {
await chat.send("next request");
const newerRunUpdate = stateUpdates.find(
- (update) => update.index === 2 && update.value?.runId === "run-2",
+ (update) =>
+ update.index === STATE_SLOT.activeRun && update.value?.runId === "run-2",
);
assert.equal(newerRunUpdate?.value.threadId, threadId);
assert.equal(newerRunUpdate?.value.status, "queued");
@@ -1971,82 +1893,7 @@ test("useChat.cancelRun completion does not clear a newer run", async () => {
assert.deepEqual(stateUpdates.slice(updatesBeforeCancelResolution), []);
});
-test("useChat.send: channel connect requests return an action without submitting a prompt", async () => {
- let createThreadCalled = false;
- let sendMessageCalled = false;
-
- const context = {
- AbortController,
- Date,
- Error,
- Map,
- Math,
- React: createReactStub(),
- addPending,
- toRenderAttachment,
- toWireAttachment,
- cancelRunRequest: async () => {},
- clearTimeout,
- createThreadRequest: async () => {
- createThreadCalled = true;
- throw new Error("connect action should not create a thread");
- },
- globalThis: {},
- listConnectableChannels: async () => ({
- channels: [
- {
- channel: "slack",
- display_name: "Slack",
- strategy: "inbound_proof_code",
- command_aliases: ["slack", "slack account"],
- action: {
- title: "Slack account connection",
- instructions: "Message the Slack app, then enter the code here.",
- },
- },
- ],
- }),
- looksLikeChannelConnectCommand,
- queryClient: {
- fetchQuery: async ({ queryFn }) => queryFn(),
- invalidateQueries: () => {},
- },
- recordAcceptedMessageRef,
- removePending,
- resolveChannelConnectCommand,
- resolveGateRequest: async () => {},
- sendMessage: async () => {
- sendMessageCalled = true;
- throw new Error("connect action should not submit a model prompt");
- },
- setInterval,
- setTimeout,
- submitManualToken: async () => {},
- useChatEvents: () => () => {},
- useHistory: () => ({
- messages: [],
- hasMore: false,
- nextCursor: null,
- isLoading: false,
- loadHistory: () => {},
- seedThreadMessages: () => {},
- setMessages: () => {},
- }),
- useSSE: () => ({ status: "idle" }),
- };
-
- runUseChatSource(context);
-
- const chat = context.globalThis.__testExports.useChat(null);
- const response = await chat.send("connect my Slack account");
-
- assert.equal(createThreadCalled, false);
- assert.equal(sendMessageCalled, false);
- assert.equal(response.channel_connect_action.channel, "slack");
- assert.equal(response.channel_connect_action.strategy, "inbound_proof_code");
-});
-
-test("useChat.send: unmatched channel connect requests submit the prompt", async () => {
+test("useChat.send: ordinary Slack chat prompts submit to the model", async () => {
let createThreadCalled = false;
let sentContent = null;
@@ -2067,28 +1914,12 @@ test("useChat.send: unmatched channel connect requests submit the prompt", async
return { thread: { thread_id: "thread-created" } };
},
globalThis: {},
- listConnectableChannels: async () => ({
- channels: [
- {
- channel: "slack",
- display_name: "Slack",
- strategy: "inbound_proof_code",
- command_aliases: ["slack", "slack account"],
- action: {
- title: "Slack account connection",
- instructions: "Message the Slack app, then enter the code here.",
- },
- },
- ],
- }),
- looksLikeChannelConnectCommand,
queryClient: {
fetchQuery: async ({ queryFn }) => queryFn(),
invalidateQueries: () => {},
},
recordAcceptedMessageRef,
removePending,
- resolveChannelConnectCommand,
resolveGateRequest: async () => {},
sendMessage: async ({ content, threadId }) => {
sentContent = content;
@@ -2118,10 +1949,10 @@ test("useChat.send: unmatched channel connect requests submit the prompt", async
runUseChatSource(context);
const chat = context.globalThis.__testExports.useChat(null);
- const response = await chat.send("connect telegram account");
+ const response = await chat.send("setup a Slack regex for chat routing");
assert.equal(createThreadCalled, true);
- assert.equal(sentContent, "connect telegram account");
+ assert.equal(sentContent, "setup a Slack regex for chat routing");
assert.equal(response.channel_connect_action, undefined);
assert.equal(response.thread_id, "thread-created");
});
@@ -2147,10 +1978,6 @@ test("useChat.send: rejected_busy appends system notice, marks optimistic failed
throw new Error("thread should already exist");
},
globalThis: {},
- listConnectableChannels: async () => {
- throw new Error("ordinary prompts should not fetch connectable channels");
- },
- looksLikeChannelConnectCommand,
queryClient: {
fetchQuery: async () => {
throw new Error("ordinary prompts should not fetch connectable channels");
@@ -2159,7 +1986,6 @@ test("useChat.send: rejected_busy appends system notice, marks optimistic failed
},
recordAcceptedMessageRef,
removePending,
- resolveChannelConnectCommand,
resolveGateRequest: async () => {},
sendMessage: async () => ({
outcome: "rejected_busy",
@@ -2200,8 +2026,8 @@ test("useChat.send: rejected_busy appends system notice, marks optimistic failed
assert.equal(userMessages[0].isOptimistic, false);
assert.equal(userMessages[0].status, "error");
- // (c) isProcessing is cleared (index 4 set to false)
- const isProcessingUpdates = stateUpdates.filter((u) => u.index === 4);
+ // (c) isProcessing is cleared.
+ const isProcessingUpdates = stateUpdatesFor(stateUpdates, STATE_SLOT.isProcessing);
const lastIsProcessing = isProcessingUpdates[isProcessingUpdates.length - 1];
assert.equal(lastIsProcessing?.value, false);
});
@@ -2227,10 +2053,6 @@ test("useChat.send: rejected_busy without notice still clears isProcessing", asy
throw new Error("thread should already exist");
},
globalThis: {},
- listConnectableChannels: async () => {
- throw new Error("ordinary prompts should not fetch connectable channels");
- },
- looksLikeChannelConnectCommand,
queryClient: {
fetchQuery: async () => {
throw new Error("ordinary prompts should not fetch connectable channels");
@@ -2239,7 +2061,6 @@ test("useChat.send: rejected_busy without notice still clears isProcessing", asy
},
recordAcceptedMessageRef,
removePending,
- resolveChannelConnectCommand,
resolveGateRequest: async () => {},
sendMessage: async () => ({
outcome: "rejected_busy",
@@ -2276,8 +2097,8 @@ test("useChat.send: rejected_busy without notice still clears isProcessing", asy
assert.equal(userMessages.length, 1);
assert.equal(userMessages[0].status, "error");
- // isProcessing is cleared (index 4 set to false)
- const isProcessingUpdates = stateUpdates.filter((u) => u.index === 4);
+ // isProcessing is cleared.
+ const isProcessingUpdates = stateUpdatesFor(stateUpdates, STATE_SLOT.isProcessing);
const lastIsProcessing = isProcessingUpdates[isProcessingUpdates.length - 1];
assert.equal(lastIsProcessing?.value, false);
});
@@ -2293,7 +2114,7 @@ test("useChat.send: active run refuses duplicate submit before network call", as
Error,
Map,
Math,
- React: createReactStub({ initialByIndex: new Map([[4, true]]) }),
+ React: createReactStub({ initialByIndex: new Map([[STATE_SLOT.isProcessing, true]]) }),
addPending,
toRenderAttachment,
toWireAttachment,
@@ -2303,10 +2124,6 @@ test("useChat.send: active run refuses duplicate submit before network call", as
throw new Error("thread should already exist");
},
globalThis: {},
- listConnectableChannels: async () => {
- throw new Error("busy prompts should not fetch connectable channels");
- },
- looksLikeChannelConnectCommand,
queryClient: {
fetchQuery: async () => {
throw new Error("busy prompts should not fetch connectable channels");
@@ -2315,7 +2132,6 @@ test("useChat.send: active run refuses duplicate submit before network call", as
},
recordAcceptedMessageRef,
removePending,
- resolveChannelConnectCommand,
resolveGateRequest: async () => {},
sendMessage: async () => {
sendCalls += 1;
@@ -2370,10 +2186,6 @@ test("useChat.send: accepted run blocks another submit until settlement", async
throw new Error("thread should already exist");
},
globalThis: {},
- listConnectableChannels: async () => {
- throw new Error("ordinary prompts should not fetch connectable channels");
- },
- looksLikeChannelConnectCommand,
queryClient: {
fetchQuery: async () => {
throw new Error("ordinary prompts should not fetch connectable channels");
@@ -2382,7 +2194,6 @@ test("useChat.send: accepted run blocks another submit until settlement", async
},
recordAcceptedMessageRef,
removePending,
- resolveChannelConnectCommand,
resolveGateRequest: async () => {},
sendMessage: async ({ content }) => {
sendCalls += 1;
@@ -2461,10 +2272,6 @@ test("useChat.send: created thread stays blocked until accepted run settles", as
return { thread: { thread_id: createdThreadId } };
},
globalThis: {},
- listConnectableChannels: async () => {
- throw new Error("ordinary prompts should not fetch connectable channels");
- },
- looksLikeChannelConnectCommand,
queryClient: {
fetchQuery: async () => {
throw new Error("ordinary prompts should not fetch connectable channels");
@@ -2474,7 +2281,6 @@ test("useChat.send: created thread stays blocked until accepted run settles", as
recordAcceptedMessageRef,
removePending,
timelineMessageIdFromAcceptedRef,
- resolveChannelConnectCommand,
resolveGateRequest: async () => {},
sendMessage: async ({ content, threadId }) => {
sendCalls += 1;
@@ -2562,10 +2368,6 @@ test("useChat.send: clears local busy when run settles before send response", as
thread: { thread_id: createdThreadId },
}),
globalThis: {},
- listConnectableChannels: async () => {
- throw new Error("ordinary prompts should not fetch connectable channels");
- },
- looksLikeChannelConnectCommand,
queryClient: {
fetchQuery: async () => {
throw new Error("ordinary prompts should not fetch connectable channels");
@@ -2575,7 +2377,6 @@ test("useChat.send: clears local busy when run settles before send response", as
recordAcceptedMessageRef,
removePending,
timelineMessageIdFromAcceptedRef,
- resolveChannelConnectCommand,
resolveGateRequest: async () => {},
sendMessage: async ({ content, threadId }) => {
sendCalls += 1;
@@ -2658,10 +2459,6 @@ test("useChat.send: clears local admission when navigating away before settlemen
throw new Error("threads already exist in this scenario");
},
globalThis: {},
- listConnectableChannels: async () => {
- throw new Error("ordinary prompts should not fetch connectable channels");
- },
- looksLikeChannelConnectCommand,
queryClient: {
fetchQuery: async () => {
throw new Error("ordinary prompts should not fetch connectable channels");
@@ -2671,7 +2468,6 @@ test("useChat.send: clears local admission when navigating away before settlemen
recordAcceptedMessageRef,
removePending,
timelineMessageIdFromAcceptedRef,
- resolveChannelConnectCommand,
resolveGateRequest: async () => {},
sendMessage: async ({ content, threadId }) => {
sendCalls += 1;
@@ -2765,10 +2561,6 @@ test("useChat.send: a send to another thread is not blocked by an unsettled run
throw new Error("threads already exist in this scenario");
},
globalThis: {},
- listConnectableChannels: async () => {
- throw new Error("ordinary prompts should not fetch connectable channels");
- },
- looksLikeChannelConnectCommand,
queryClient: {
fetchQuery: async () => {
throw new Error("ordinary prompts should not fetch connectable channels");
@@ -2778,7 +2570,6 @@ test("useChat.send: a send to another thread is not blocked by an unsettled run
recordAcceptedMessageRef,
removePending,
timelineMessageIdFromAcceptedRef,
- resolveChannelConnectCommand,
resolveGateRequest: async () => {},
sendMessage: async ({ threadId }) => {
sendCalls += 1;
@@ -2832,7 +2623,7 @@ test("useChat.send: a send to another thread is not blocked by an unsettled run
assert.equal(sendCalls, 2, "sendMessage must be called for the second, different-thread send");
});
-test("useChat.send: connectable channel fetch failures submit the prompt", async () => {
+test("useChat.send: slash connect text submits to the model without fetching connectable channels", async () => {
let createThreadCalled = false;
let sentContent = null;
const loggedErrors = [];
@@ -2857,17 +2648,12 @@ test("useChat.send: connectable channel fetch failures submit the prompt", async
return { thread: { thread_id: "thread-created" } };
},
globalThis: {},
- listConnectableChannels: async () => {
- throw new Error("connectable channel service unavailable");
- },
- looksLikeChannelConnectCommand,
queryClient: {
fetchQuery: async ({ queryFn }) => queryFn(),
invalidateQueries: () => {},
},
recordAcceptedMessageRef,
removePending,
- resolveChannelConnectCommand,
resolveGateRequest: async () => {},
sendMessage: async ({ content, threadId }) => {
sentContent = content;
@@ -2897,13 +2683,13 @@ test("useChat.send: connectable channel fetch failures submit the prompt", async
runUseChatSource(context);
const chat = context.globalThis.__testExports.useChat(null);
- const response = await chat.send("connect my Slack account");
+ const response = await chat.send("/connect my Slack account");
assert.equal(createThreadCalled, true);
- assert.equal(sentContent, "connect my Slack account");
+ assert.equal(sentContent, "/connect my Slack account");
assert.equal(response.channel_connect_action, undefined);
assert.equal(response.thread_id, "thread-created");
- assert.equal(loggedErrors[0][0], "Failed to resolve connectable channels:");
+ assert.deepEqual(loggedErrors, []);
});
function createResolveGateContext({
@@ -2916,7 +2702,7 @@ function createResolveGateContext({
},
} = {}) {
// useChat state call order: cooldownUntil(0), now(1), activeRun(2),
- // channelConnectAction(3), isProcessing(4), pendingGate(5).
+ // isProcessing(3), pendingGate(4).
const pendingGate = {
runId: "run-1",
gateRef: "gate-1",
@@ -2932,9 +2718,9 @@ function createResolveGateContext({
Math,
React: createReactStub({
initialByIndex: new Map([
- [2, { runId: "run-1", threadId: "thread-1", status: "running" }],
- [4, true],
- [5, pendingGate],
+ [STATE_SLOT.activeRun, { runId: "run-1", threadId: "thread-1", status: "running" }],
+ [STATE_SLOT.isProcessing, true],
+ [STATE_SLOT.pendingGate, pendingGate],
]),
setCalls: stateUpdates,
}),
@@ -2947,15 +2733,12 @@ function createResolveGateContext({
throw new Error("createThread should not run");
},
globalThis: {},
- listConnectableChannels: async () => ({ channels: [] }),
- looksLikeChannelConnectCommand,
queryClient: {
fetchQuery: async () => ({ channels: [] }),
invalidateQueries: () => {},
},
recordAcceptedMessageRef,
removePending,
- resolveChannelConnectCommand,
resolveGateRequest: async () => resolveGateResponse,
sendMessage: async () => {
throw new Error("sendMessage should not run");
@@ -2989,21 +2772,22 @@ test("useChat.resolveGate: denied keeps isProcessing true and does not clear act
const chat = context.globalThis.__testExports.useChat("thread-1");
await chat.resolveGate("denied");
- // pendingGate (index 5) is cleared
- const pendingGateUpdates = stateUpdates.filter((u) => u.index === 5);
+ // pendingGate is cleared.
+ const pendingGateUpdates = stateUpdatesFor(stateUpdates, STATE_SLOT.pendingGate);
assert.equal(pendingGateUpdates.length, 1);
assert.equal(pendingGateUpdates[0].value, null);
- // isProcessing (index 4) is set to true — run continues
- const isProcessingUpdates = stateUpdates.filter((u) => u.index === 4);
+ // isProcessing is set to true — run continues.
+ const isProcessingUpdates = stateUpdatesFor(stateUpdates, STATE_SLOT.isProcessing);
assert.ok(isProcessingUpdates.length > 0, "isProcessing should be updated");
const lastIsProcessing = isProcessingUpdates[isProcessingUpdates.length - 1];
assert.equal(lastIsProcessing.value, true);
- // activeRun (index 2) is NOT cleared by resolveGate
- const activeRunClears = stateUpdates.filter(
- (u) => u.index === 2 && u.value === null,
- );
+ // activeRun is NOT cleared by resolveGate.
+ const activeRunClears = stateUpdatesFor(
+ stateUpdates,
+ STATE_SLOT.activeRun,
+ ).filter((u) => u.value === null);
assert.equal(activeRunClears.length, 0, "resolveGate must not clear activeRun");
assert.deepEqual(
JSON.parse(JSON.stringify(
@@ -3022,16 +2806,17 @@ test("useChat.resolveGate: resumed cancelled auth keeps processing until follow-
const chat = context.globalThis.__testExports.useChat("thread-1");
await chat.resolveGate("cancelled");
- // isProcessing (index 4) is set to true — run continues
- const isProcessingUpdates = stateUpdates.filter((u) => u.index === 4);
+ // isProcessing is set to true — run continues.
+ const isProcessingUpdates = stateUpdatesFor(stateUpdates, STATE_SLOT.isProcessing);
assert.ok(isProcessingUpdates.length > 0, "isProcessing should be updated");
const lastIsProcessing = isProcessingUpdates[isProcessingUpdates.length - 1];
assert.equal(lastIsProcessing.value, true);
- // activeRun (index 2) is NOT cleared
- const activeRunClears = stateUpdates.filter(
- (u) => u.index === 2 && u.value === null,
- );
+ // activeRun is NOT cleared.
+ const activeRunClears = stateUpdatesFor(
+ stateUpdates,
+ STATE_SLOT.activeRun,
+ ).filter((u) => u.value === null);
assert.equal(activeRunClears.length, 0, "resolveGate must not clear activeRun");
assert.deepEqual(
JSON.parse(JSON.stringify(
@@ -3058,14 +2843,14 @@ test("useChat.resolveGate: terminal cancelled clears processing and activeRun",
const chat = context.globalThis.__testExports.useChat("thread-1");
await chat.resolveGate("cancelled");
- const isProcessingUpdates = stateUpdates.filter((u) => u.index === 4);
+ const isProcessingUpdates = stateUpdatesFor(stateUpdates, STATE_SLOT.isProcessing);
assert.ok(isProcessingUpdates.length > 0, "isProcessing should be updated");
assert.equal(isProcessingUpdates[isProcessingUpdates.length - 1].value, false);
- const pendingGateUpdates = stateUpdates.filter((u) => u.index === 5);
+ const pendingGateUpdates = stateUpdatesFor(stateUpdates, STATE_SLOT.pendingGate);
assert.equal(pendingGateUpdates[pendingGateUpdates.length - 1].value, null);
- const activeRunUpdates = stateUpdates.filter((u) => u.index === 2);
+ const activeRunUpdates = stateUpdates.filter((u) => u.index === STATE_SLOT.activeRun);
assert.equal(activeRunUpdates[activeRunUpdates.length - 1].value, null);
assert.deepEqual(
JSON.parse(JSON.stringify(
@@ -3084,7 +2869,7 @@ test("useChat.resolveGate: approved also keeps isProcessing true", async () => {
const chat = context.globalThis.__testExports.useChat("thread-1");
await chat.resolveGate("approved");
- const isProcessingUpdates = stateUpdates.filter((u) => u.index === 4);
+ const isProcessingUpdates = stateUpdatesFor(stateUpdates, STATE_SLOT.isProcessing);
assert.ok(isProcessingUpdates.length > 0);
const lastIsProcessing = isProcessingUpdates[isProcessingUpdates.length - 1];
assert.equal(lastIsProcessing.value, true);
@@ -3117,18 +2902,15 @@ function createParallelSendContext({
let currentMessages = [];
const seededByThread = new Map();
const initialByIndex = new Map();
- // State slot order: cooldownUntil(0), now(1), activeRun(2),
- // channelConnectAction(3), isProcessing(4), pendingGate(5),
- // busyGateNotice(6), stateThreadId(7).
if (activeRun !== undefined) {
- initialByIndex.set(2, activeRun);
+ initialByIndex.set(STATE_SLOT.activeRun, activeRun);
}
// A thread with an in-flight run carries BOTH activeRun and isProcessing.
// Seeding isProcessing for the viewed-thread cases is what makes these
// fixtures reproduce the real busy state rather than a half-state the
// `isProcessing` early-return would never trip.
if (isProcessing !== undefined) {
- initialByIndex.set(4, isProcessing);
+ initialByIndex.set(STATE_SLOT.isProcessing, isProcessing);
}
const context = {
@@ -3148,10 +2930,6 @@ function createParallelSendContext({
return { thread: { thread_id: createdThreadId } };
},
globalThis: {},
- listConnectableChannels: async () => {
- throw new Error("ordinary prompts should not fetch connectable channels");
- },
- looksLikeChannelConnectCommand,
queryClient: {
fetchQuery: async () => {
throw new Error("ordinary prompts should not fetch connectable channels");
@@ -3161,7 +2939,6 @@ function createParallelSendContext({
recordAcceptedMessageRef,
removePending,
timelineMessageIdFromAcceptedRef,
- resolveChannelConnectCommand,
resolveGateRequest: async () => {},
sendMessage: async (body) => {
sentBody = body;