Interactive Telegram Node Browser and Mid-Graph Executor - #9
Conversation
Features implemented: - Added '/workflows', '📋 Workflows', and '/browse' commands to start the browser. - Created an in-place pager using Telegram edited message text and inline keyboards. - Developed a high-fidelity mathematically indented graph hierarchy visualizer. - Supported secure 'Run from here' mid-graph workflow execution with '/confirm' token and 'start_node_id' dispatch. - Implemented node configuration editing and cascade node deletion in D1. - Extended 'scripts/test-builder-e2e.js' with all 22 tests. - Re-verified full test suites pass completely. Co-authored-by: aethelred-agent-factory <238771426+aethelred-agent-factory@users.noreply.github.com>
|
👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
📝 WalkthroughWalkthroughThe PR adds Telegram workflow browsing with authorization, pagination, node editing and deletion, supports execution from a selected node through GitHub dispatch and the executor, and expands end-to-end tests for these flows. ChangesWorkflow browser and execution control
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant Telegram
participant Worker
participant D1
participant KV
participant GitHubActions
participant Executor
User->>Telegram: browse workflow
Telegram->>Worker: command or callback
Worker->>D1: load workflow and node state
Worker->>KV: store browse or pending execution state
Worker->>Telegram: render node actions
User->>Telegram: run from selected node
Telegram->>Worker: confirm execution
Worker->>GitHubActions: dispatch with start_node_id
GitHubActions->>Executor: provide client_payload
Executor->>Executor: start at selected node
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
worker/src/index.js (1)
1540-1561: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
sendMessageAndReturnduplicatessendMessage(lines 2082-2100).Both functions build the same request body and call the same
sendMessageTelegram endpoint with near-identical error handling; the only difference is the return value. Consider havingsendMessagedelegate tosendMessageAndReturn(or vice versa) to avoid two copies of this logic diverging over time.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@worker/src/index.js` around lines 1540 - 1561, Refactor sendMessage and sendMessageAndReturn to share one implementation for constructing the request, calling Telegram’s sendMessage endpoint, and handling errors. Have one function delegate to the other while preserving sendMessageAndReturn’s response behavior and sendMessage’s existing caller-facing behavior; remove the duplicated endpoint logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@worker/src/index.js`:
- Around line 1837-1853: Update handleCancel to delete the edit_state:${chatId}
entry from WORKFLOW_STATE alongside pending:${chatId} and builder:${chatId},
ensuring /cancel fully clears node-edit mode before sending the existing
“Discarded.” message.
- Around line 870-882: Update the fallback flow around handleNodeConfigEdit and
its edit-state handling so non-text messages do not reach text.trim() with an
undefined value. Validate message.text before processing active edits and return
the existing graceful prompt or handled result for stickers, photos, voice
messages, and other non-text inputs, while preserving normal text edit behavior.
- Around line 1588-1631: Update the wf_sel: callback path in
handleBrowserCallback to answer the callback query after invoking startBrowsing,
and update startBrowsing’s DB-missing, workflow-not-found, and no-nodes branches
to use editMessage with messageIdToEdit when provided, falling back to
sendMessage otherwise. Preserve the existing success rendering behavior and
ensure every workflow-selection callback is answered.
- Around line 1882-1896: Update the callback handling around editMessage so the
success notification is sent only when the edit succeeds. Handle network errors
and all non-OK responses, including non-429 HTTP failures, as failures; preserve
the existing rate-limit alert for status 429 and provide an appropriate failure
response for other errors instead of announcing that paging succeeded.
- Around line 1854-1880: Make node and edge deletion atomic in the wf_nav:delete
branch by replacing the two sequential env.DB.prepare(...).run() calls with a
single env.DB.batch([...]) containing both DELETE statements. Preserve the
existing workflow_id and selectedNode.id conditions and keep the subsequent
callback, remaining-node query, and state handling unchanged.
---
Nitpick comments:
In `@worker/src/index.js`:
- Around line 1540-1561: Refactor sendMessage and sendMessageAndReturn to share
one implementation for constructing the request, calling Telegram’s sendMessage
endpoint, and handling errors. Have one function delegate to the other while
preserving sendMessageAndReturn’s response behavior and sendMessage’s existing
caller-facing behavior; remove the duplicated endpoint logic.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: fb9c4aef-38c9-4c38-a45d-6963cacd60da
📒 Files selected for processing (3)
scripts/executor.jsscripts/test-builder-e2e.jsworker/src/index.js
| } else { | ||
| const handled = await handleBuilderState(env, chatId, message.text); | ||
| if (!handled) { | ||
| if (message.text) { | ||
| await sendMessage(env, chatId, | ||
| "Send me a workflow as a .json file attachment. I'll validate it and show you " + | ||
| "a summary — nothing runs until you reply /confirm <token>. Send /whoami to see your chat ID.\n\n" + | ||
| "Or design a workflow directly in Telegram using: /newworkflow"); | ||
| const editHandled = await handleNodeConfigEdit(env, chatId, message.text); | ||
| if (!editHandled) { | ||
| const handled = await handleBuilderState(env, chatId, message.text); | ||
| if (!handled) { | ||
| if (message.text) { | ||
| await sendMessage(env, chatId, | ||
| "Send me a workflow as a .json file attachment. I'll validate it and show you " + | ||
| "a summary — nothing runs until you reply /confirm <token>. Send /whoami to see your chat ID.\n\n" + | ||
| "Or design a workflow directly in Telegram using: /newworkflow"); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
handleNodeConfigEdit crashes on non-text messages sent during an active edit session.
The fallback path calls handleNodeConfigEdit(env, chatId, message.text) unconditionally; message.text is undefined for stickers/photos/voice/etc. If an edit_state is active, text.trim() at line 1923 throws TypeError: Cannot read properties of undefined (reading 'trim'). It's caught by the outer try/catch and surfaced as a raw Error: ... message rather than a graceful prompt.
🔧 Proposed fix
const editState = JSON.parse(rawState);
const { workflow_id, node_id, message_id } = editState;
+ if (typeof text !== 'string') {
+ await sendMessage(env, chatId, "Please send the new configuration as a text message.");
+ return true;
+ }
+
if (!env.DB) {Also applies to: 1899-1925
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@worker/src/index.js` around lines 870 - 882, Update the fallback flow around
handleNodeConfigEdit and its edit-state handling so non-text messages do not
reach text.trim() with an undefined value. Validate message.text before
processing active edits and return the existing graceful prompt or handled
result for stickers, photos, voice messages, and other non-text inputs, while
preserving normal text edit behavior.
| async function startBrowsing(env, chatId, workflowId, messageIdToEdit = null) { | ||
| if (!env.DB) { | ||
| await sendMessage(env, chatId, "Database is not configured."); | ||
| return; | ||
| } | ||
|
|
||
| const workflow = await env.DB.prepare( | ||
| "SELECT id, name FROM workflows WHERE id = ?" | ||
| ).bind(workflowId).first(); | ||
|
|
||
| if (!workflow) { | ||
| await sendMessage(env, chatId, `Workflow with ID "${workflowId}" not found.`); | ||
| return; | ||
| } | ||
|
|
||
| const nodes = await env.DB.prepare( | ||
| "SELECT id FROM nodes WHERE workflow_id = ? ORDER BY id ASC" | ||
| ).bind(workflowId).all(); | ||
|
|
||
| if (!nodes.results || nodes.results.length === 0) { | ||
| await sendMessage(env, chatId, `Workflow "${workflow.name}" has no nodes to browse.`); | ||
| return; | ||
| } | ||
|
|
||
| const browseState = { | ||
| workflow_id: workflowId, | ||
| node_index: 0, | ||
| message_id: messageIdToEdit | ||
| }; | ||
|
|
||
| const rendered = await renderNodeBrowser(env, chatId, workflowId, 0); | ||
| if (!rendered) return; | ||
|
|
||
| if (messageIdToEdit) { | ||
| await editMessage(env, chatId, messageIdToEdit, rendered.text, rendered.reply_markup); | ||
| } else { | ||
| const sentMsg = await sendMessageAndReturn(env, chatId, rendered.text, rendered.reply_markup); | ||
| if (sentMsg && sentMsg.result) { | ||
| browseState.message_id = sentMsg.result.message_id; | ||
| } | ||
| } | ||
|
|
||
| await env.WORKFLOW_STATE.put(`browse:${chatId}`, JSON.stringify(browseState)); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
wf_sel: workflow selection never answers the callback query.
handleBrowserCallback's wf_sel: branch calls startBrowsing(...) and returns, but neither it nor startBrowsing ever calls answerCallbackQuery. Per the Telegram Bot API, clients show a loading spinner on the pressed button until the query is answered (or ~10-15s until it expires) — this fires on every single workflow selection, the primary entry point of this feature. Additionally, startBrowsing's error branches (DB not configured / workflow not found / no nodes) always call sendMessage, even when messageIdToEdit is supplied (i.e. called from this callback context) — leaving the original message with its now-stale inline keyboard unedited, on top of the missing callback answer.
🔧 Proposed fix
if (data.startsWith('wf_sel:')) {
const workflowId = data.substring(7);
- await startBrowsing(env, chatId, workflowId, callbackQuery.message.message_id);
+ await startBrowsing(env, chatId, workflowId, callbackQuery.message.message_id);
+ await answerCallbackQuery(env, callbackQuery.id);
return;
}And inside startBrowsing, prefer editMessage over sendMessage in the error branches whenever messageIdToEdit is set.
Also applies to: 1722-1730
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@worker/src/index.js` around lines 1588 - 1631, Update the wf_sel: callback
path in handleBrowserCallback to answer the callback query after invoking
startBrowsing, and update startBrowsing’s DB-missing, workflow-not-found, and
no-nodes branches to use editMessage with messageIdToEdit when provided, falling
back to sendMessage otherwise. Preserve the existing success rendering behavior
and ensure every workflow-selection callback is answered.
| } else if (data === 'wf_nav:edit') { | ||
| const selectedNode = nodesRows[nodeIndex]; | ||
| const editState = { | ||
| workflow_id: workflowId, | ||
| node_id: selectedNode.id, | ||
| state: 'EDITING_NODE_CONFIG', | ||
| message_id: callbackQuery.message.message_id | ||
| }; | ||
|
|
||
| await env.WORKFLOW_STATE.put(`edit_state:${chatId}`, JSON.stringify(editState)); | ||
| await answerCallbackQuery(env, callbackQuery.id, { text: "Editing node..." }); | ||
| await sendMessage( | ||
| env, chatId, | ||
| `Editing configuration for node [${selectedNode.id}] (${selectedNode.type}).\n` + | ||
| `Send the new configuration as a JSON object, or a single text/numeric value (e.g., the URL, command, ms, or message):` | ||
| ); | ||
| return; |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
/cancel doesn't clear edit_state:${chatId}, so "cancelling" a node edit silently leaves it armed.
Once wf_nav:edit stores edit_state:${chatId} (with no TTL), the only escape hatch a user has is /cancel — but handleCancel only deletes pending:${chatId} and builder:${chatId}. After sending /cancel and seeing "Discarded.", the next ordinary text message the user sends (which doesn't match any of the special-cased prefixes) is routed through handleNodeConfigEdit first and gets silently written into the node's config, overwriting it without the user's intent.
🔧 Proposed fix (outside this range, in the existing handleCancel)
async function handleCancel(env, chatId) {
await env.WORKFLOW_STATE.delete(`pending:${chatId}`);
await env.WORKFLOW_STATE.delete(`builder:${chatId}`);
await env.WORKFLOW_STATE.delete(`edit_state:${chatId}`);
await sendMessage(env, chatId, 'Discarded.');
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@worker/src/index.js` around lines 1837 - 1853, Update handleCancel to delete
the edit_state:${chatId} entry from WORKFLOW_STATE alongside pending:${chatId}
and builder:${chatId}, ensuring /cancel fully clears node-edit mode before
sending the existing “Discarded.” message.
| } else if (data === 'wf_nav:delete') { | ||
| const selectedNode = nodesRows[nodeIndex]; | ||
|
|
||
| await env.DB.prepare( | ||
| "DELETE FROM nodes WHERE workflow_id = ? AND id = ?" | ||
| ).bind(workflowId, selectedNode.id).run(); | ||
|
|
||
| await env.DB.prepare( | ||
| "DELETE FROM edges WHERE workflow_id = ? AND (source = ? OR target = ?)" | ||
| ).bind(workflowId, selectedNode.id, selectedNode.id).run(); | ||
|
|
||
| await answerCallbackQuery(env, callbackQuery.id, { text: "Node deleted!" }); | ||
|
|
||
| const { results: remainingNodes } = await env.DB.prepare( | ||
| "SELECT id FROM nodes WHERE workflow_id = ? ORDER BY id ASC" | ||
| ).bind(workflowId).all(); | ||
|
|
||
| if (!remainingNodes || remainingNodes.length === 0) { | ||
| await env.WORKFLOW_STATE.delete(`browse:${chatId}`); | ||
| await editMessage(env, chatId, callbackQuery.message.message_id, `🔧 ${workflow.name} has no nodes left.`); | ||
| return; | ||
| } | ||
|
|
||
| if (nodeIndex >= remainingNodes.length) { | ||
| nodeIndex = remainingNodes.length - 1; | ||
| } | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Node/edge deletion isn't atomic.
The DELETE FROM nodes and DELETE FROM edges calls are two separate, sequential .run() calls. If the process fails between them (worker eviction, D1 transient error), the node is gone but its edges remain — dangling edges that no longer round-trip cleanly (the graph renderer masks this by labelling the missing target 'unknown', but /api/workflows/:id and dispatch payloads would still carry the orphaned edge). The rest of the codebase already uses env.DB.batch([...]) for this kind of multi-statement write (e.g. handleConfirm's node/edge inserts), and Cloudflare documents batched statements as transactional (all-or-nothing).
🔧 Proposed fix
- await env.DB.prepare(
- "DELETE FROM nodes WHERE workflow_id = ? AND id = ?"
- ).bind(workflowId, selectedNode.id).run();
-
- await env.DB.prepare(
- "DELETE FROM edges WHERE workflow_id = ? AND (source = ? OR target = ?)"
- ).bind(workflowId, selectedNode.id, selectedNode.id).run();
+ await env.DB.batch([
+ env.DB.prepare("DELETE FROM nodes WHERE workflow_id = ? AND id = ?").bind(workflowId, selectedNode.id),
+ env.DB.prepare("DELETE FROM edges WHERE workflow_id = ? AND (source = ? OR target = ?)").bind(workflowId, selectedNode.id, selectedNode.id)
+ ]);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| } else if (data === 'wf_nav:delete') { | |
| const selectedNode = nodesRows[nodeIndex]; | |
| await env.DB.prepare( | |
| "DELETE FROM nodes WHERE workflow_id = ? AND id = ?" | |
| ).bind(workflowId, selectedNode.id).run(); | |
| await env.DB.prepare( | |
| "DELETE FROM edges WHERE workflow_id = ? AND (source = ? OR target = ?)" | |
| ).bind(workflowId, selectedNode.id, selectedNode.id).run(); | |
| await answerCallbackQuery(env, callbackQuery.id, { text: "Node deleted!" }); | |
| const { results: remainingNodes } = await env.DB.prepare( | |
| "SELECT id FROM nodes WHERE workflow_id = ? ORDER BY id ASC" | |
| ).bind(workflowId).all(); | |
| if (!remainingNodes || remainingNodes.length === 0) { | |
| await env.WORKFLOW_STATE.delete(`browse:${chatId}`); | |
| await editMessage(env, chatId, callbackQuery.message.message_id, `🔧 ${workflow.name} has no nodes left.`); | |
| return; | |
| } | |
| if (nodeIndex >= remainingNodes.length) { | |
| nodeIndex = remainingNodes.length - 1; | |
| } | |
| } | |
| } else if (data === 'wf_nav:delete') { | |
| const selectedNode = nodesRows[nodeIndex]; | |
| await env.DB.batch([ | |
| env.DB.prepare("DELETE FROM nodes WHERE workflow_id = ? AND id = ?").bind(workflowId, selectedNode.id), | |
| env.DB.prepare("DELETE FROM edges WHERE workflow_id = ? AND (source = ? OR target = ?)").bind(workflowId, selectedNode.id, selectedNode.id) | |
| ]); | |
| await answerCallbackQuery(env, callbackQuery.id, { text: "Node deleted!" }); | |
| const { results: remainingNodes } = await env.DB.prepare( | |
| "SELECT id FROM nodes WHERE workflow_id = ? ORDER BY id ASC" | |
| ).bind(workflowId).all(); | |
| if (!remainingNodes || remainingNodes.length === 0) { | |
| await env.WORKFLOW_STATE.delete(`browse:${chatId}`); | |
| await editMessage(env, chatId, callbackQuery.message.message_id, `🔧 ${workflow.name} has no nodes left.`); | |
| return; | |
| } | |
| if (nodeIndex >= remainingNodes.length) { | |
| nodeIndex = remainingNodes.length - 1; | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@worker/src/index.js` around lines 1854 - 1880, Make node and edge deletion
atomic in the wf_nav:delete branch by replacing the two sequential
env.DB.prepare(...).run() calls with a single env.DB.batch([...]) containing
both DELETE statements. Preserve the existing workflow_id and selectedNode.id
conditions and keep the subsequent callback, remaining-node query, and state
handling unchanged.
| browseState.node_index = nodeIndex; | ||
| await env.WORKFLOW_STATE.put(`browse:${chatId}`, JSON.stringify(browseState)); | ||
|
|
||
| const rendered = await renderNodeBrowser(env, chatId, workflowId, nodeIndex); | ||
| if (rendered) { | ||
| const editRes = await editMessage(env, chatId, callbackQuery.message.message_id, rendered.text, rendered.reply_markup); | ||
| if (editRes && !editRes.ok && editRes.status === 429) { | ||
| await answerCallbackQuery(env, callbackQuery.id, { | ||
| text: "Too fast! Please wait a moment...", | ||
| show_alert: true | ||
| }); | ||
| } else { | ||
| await answerCallbackQuery(env, callbackQuery.id, { text: `Paged to node ${nodeIndex + 1}` }); | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
editMessage failures other than HTTP 429 are reported to the user as success.
The condition only distinguishes editRes.status === 429; any other failure (network error from the catch block, or a non-429 HTTP error such as "message is not modified") falls into the else branch, which announces "Paged to node ${nodeIndex + 1}" as if the edit succeeded, even though the displayed message may be stale/unchanged.
🔧 Proposed fix
const rendered = await renderNodeBrowser(env, chatId, workflowId, nodeIndex);
if (rendered) {
const editRes = await editMessage(env, chatId, callbackQuery.message.message_id, rendered.text, rendered.reply_markup);
- if (editRes && !editRes.ok && editRes.status === 429) {
+ if (editRes && editRes.ok) {
+ await answerCallbackQuery(env, callbackQuery.id, { text: `Paged to node ${nodeIndex + 1}` });
+ } else if (editRes && editRes.status === 429) {
await answerCallbackQuery(env, callbackQuery.id, {
text: "Too fast! Please wait a moment...",
show_alert: true
});
} else {
- await answerCallbackQuery(env, callbackQuery.id, { text: `Paged to node ${nodeIndex + 1}` });
+ await answerCallbackQuery(env, callbackQuery.id, {
+ text: "Failed to update. Please try again.",
+ show_alert: true
+ });
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| browseState.node_index = nodeIndex; | |
| await env.WORKFLOW_STATE.put(`browse:${chatId}`, JSON.stringify(browseState)); | |
| const rendered = await renderNodeBrowser(env, chatId, workflowId, nodeIndex); | |
| if (rendered) { | |
| const editRes = await editMessage(env, chatId, callbackQuery.message.message_id, rendered.text, rendered.reply_markup); | |
| if (editRes && !editRes.ok && editRes.status === 429) { | |
| await answerCallbackQuery(env, callbackQuery.id, { | |
| text: "Too fast! Please wait a moment...", | |
| show_alert: true | |
| }); | |
| } else { | |
| await answerCallbackQuery(env, callbackQuery.id, { text: `Paged to node ${nodeIndex + 1}` }); | |
| } | |
| } | |
| browseState.node_index = nodeIndex; | |
| await env.WORKFLOW_STATE.put(`browse:${chatId}`, JSON.stringify(browseState)); | |
| const rendered = await renderNodeBrowser(env, chatId, workflowId, nodeIndex); | |
| if (rendered) { | |
| const editRes = await editMessage(env, chatId, callbackQuery.message.message_id, rendered.text, rendered.reply_markup); | |
| if (editRes && editRes.ok) { | |
| await answerCallbackQuery(env, callbackQuery.id, { text: `Paged to node ${nodeIndex + 1}` }); | |
| } else if (editRes && editRes.status === 429) { | |
| await answerCallbackQuery(env, callbackQuery.id, { | |
| text: "Too fast! Please wait a moment...", | |
| show_alert: true | |
| }); | |
| } else { | |
| await answerCallbackQuery(env, callbackQuery.id, { | |
| text: "Failed to update. Please try again.", | |
| show_alert: true | |
| }); | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@worker/src/index.js` around lines 1882 - 1896, Update the callback handling
around editMessage so the success notification is sent only when the edit
succeeds. Handle network errors and all non-OK responses, including non-429 HTTP
failures, as failures; preserve the existing rate-limit alert for status 429 and
provide an appropriate failure response for other errors instead of announcing
that paging succeeded.
An interactive, slide-like node browsing and editing experience for Telegram Workflows, supporting graph branch visualizer, inline navigation, configuration edits, cascade node deletions, and mid-graph run triggers.
PR created automatically by Jules for task 9490534594586566844 started by @aethelred-agent-factory
Summary by CodeRabbit
New Features
/workflowsand/browsecommands for listing and exploring workflows.Bug Fixes
Tests