From 0f051ea4675030fbdf80da0095780d9dbfe522b2 Mon Sep 17 00:00:00 2001 From: tusharmagar Date: Tue, 21 Apr 2026 13:02:44 +0530 Subject: [PATCH 1/3] fix: duplicate navigation button --- apps/x/apps/renderer/src/App.tsx | 45 +++----------------------------- 1 file changed, 4 insertions(+), 41 deletions(-) diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index f933b604..602c0956 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -127,8 +127,8 @@ const TITLEBAR_BUTTON_PX = 32 const TITLEBAR_BUTTON_GAP_PX = 4 const TITLEBAR_HEADER_GAP_PX = 8 const TITLEBAR_TOGGLE_MARGIN_LEFT_PX = 12 -const TITLEBAR_BUTTONS_COLLAPSED = 4 -const TITLEBAR_BUTTON_GAPS_COLLAPSED = 3 +const TITLEBAR_BUTTONS_COLLAPSED = 1 +const TITLEBAR_BUTTON_GAPS_COLLAPSED = 0 const GRAPH_TAB_PATH = '__rowboat_graph_view__' const SUGGESTED_TOPICS_TAB_PATH = '__rowboat_suggested_topics__' const BASES_DEFAULT_TAB_PATH = '__rowboat_bases_default__' @@ -506,22 +506,13 @@ function viewStatesEqual(a: ViewState, b: ViewState): boolean { return true // both graph } -/** Sidebar toggle + utility buttons (fixed position, top-left) */ +/** Sidebar toggle (fixed position, top-left) */ function FixedSidebarToggle({ - onNavigateBack, - onNavigateForward, - canNavigateBack, - canNavigateForward, leftInsetPx, }: { - onNavigateBack: () => void - onNavigateForward: () => void - canNavigateBack: boolean - canNavigateForward: boolean leftInsetPx: number }) { - const { toggleSidebar, state } = useSidebar() - const isCollapsed = state === "collapsed" + const { toggleSidebar } = useSidebar() return (
) } @@ -4756,10 +4723,6 @@ function App() { )} {/* Rendered last so its no-drag region paints over the sidebar drag region */} { void navigateBack() }} - onNavigateForward={() => { void navigateForward() }} - canNavigateBack={canNavigateBack} - canNavigateForward={canNavigateForward} leftInsetPx={isMac ? MACOS_TRAFFIC_LIGHTS_RESERVED_PX : 0} /> From c81d3cb27b73e1b485a9ade643ac1f794f1b73a2 Mon Sep 17 00:00:00 2001 From: Ramnique Singh <30795890+ramnique@users.noreply.github.com> Date: Tue, 21 Apr 2026 14:36:00 +0530 Subject: [PATCH 2/3] surface silent runtime failures as error events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AgentRuntime.trigger() wrapped its body in try/finally with no outer catch. An inner catch around the streamAgent for-await only handled AbortError and rethrew everything else. Call sites fire-and-forget trigger (runs.ts:26,60,72), so any thrown error became an unhandled promise rejection. The finally still ran and published run-processing-end, but nothing told the renderer why — the chat showed the spinner, then an empty assistant bubble. Provider misconfig, invalid API keys, unknown model ids, streamText setup throws, runsRepo.fetch or loadAgent failing, and provider auth/rate-limit rejections on the first chunk all hit this path on a first message. All invisible. Add a top-level catch that formats the error to a string and emits a {type: "error"} RunEvent via the existing runsRepo/bus path. The renderer already renders those as a chat bubble plus toast (App.tsx:2069) — no UI work needed. No changes to the abort path: user-initiated stops still flow through the existing inner catch and the signal.aborted branch that emits run-stopped. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/x/packages/core/src/agents/runtime.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/apps/x/packages/core/src/agents/runtime.ts b/apps/x/packages/core/src/agents/runtime.ts index f978449b..81421358 100644 --- a/apps/x/packages/core/src/agents/runtime.ts +++ b/apps/x/packages/core/src/agents/runtime.ts @@ -194,6 +194,19 @@ export class AgentRuntime implements IAgentRuntime { await this.runsRepo.appendEvents(runId, [stoppedEvent]); await this.bus.publish(stoppedEvent); } + } catch (error) { + console.error(`Run ${runId} failed:`, error); + const message = error instanceof Error + ? (error.stack || error.message || error.name) + : typeof error === "string" ? error : JSON.stringify(error); + const errorEvent: z.infer = { + runId, + type: "error", + error: message, + subflow: [], + }; + await this.runsRepo.appendEvents(runId, [errorEvent]); + await this.bus.publish(errorEvent); } finally { this.abortRegistry.cleanup(runId); await this.runsLock.release(runId); From 15567cd1dd47190ff165a76b28dd888e83e942a3 Mon Sep 17 00:00:00 2001 From: Ramnique Singh <30795890+ramnique@users.noreply.github.com> Date: Tue, 21 Apr 2026 14:38:19 +0530 Subject: [PATCH 3/3] let tool failures be observed by the model instead of killing the run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit streamAgent executed tools with no try/catch around the call. A throw from execTool or from a subflow agent streamed up through streamAgent, out of trigger's inner catch (which rethrows non-abort errors), and into the new top-level catch that the previous commit added. That surfaces the failure — but it ends the run. One misbehaving tool took down the whole conversation. Wrap the tool-execution block in a try/catch. On abort, rethrow so the existing AbortError path still fires. On any other error, convert the exception into a tool-result payload ({ success: false, error, toolName }) and keep going. The model then sees a tool-result message saying the tool failed with a specific message and can apologize, retry with different arguments, pick a different tool, or explain to the user — the normal recovery moves it already knows how to make. No change to happy-path tool execution, no change to abort handling, no change to subflow agent semantics (subflows that themselves error are treated identically to regular tool errors at the call site). Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/x/packages/core/src/agents/runtime.ts | 51 ++++++++++++++-------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/apps/x/packages/core/src/agents/runtime.ts b/apps/x/packages/core/src/agents/runtime.ts index 81421358..ae69d60c 100644 --- a/apps/x/packages/core/src/agents/runtime.ts +++ b/apps/x/packages/core/src/agents/runtime.ts @@ -955,27 +955,40 @@ export async function* streamAgent({ subflow: [], }); let result: unknown = null; - if (agent.tools![toolCall.toolName].type === "agent") { - const subflowState = state.subflowStates[toolCallId]; - for await (const event of streamAgent({ - state: subflowState, - idGenerator, - runId, - messageQueue, - modelConfigRepo, - signal, - abortRegistry, - })) { - yield* processEvent({ - ...event, - subflow: [toolCallId, ...event.subflow], - }); + try { + if (agent.tools![toolCall.toolName].type === "agent") { + const subflowState = state.subflowStates[toolCallId]; + for await (const event of streamAgent({ + state: subflowState, + idGenerator, + runId, + messageQueue, + modelConfigRepo, + signal, + abortRegistry, + })) { + yield* processEvent({ + ...event, + subflow: [toolCallId, ...event.subflow], + }); + } + if (!subflowState.getPendingAskHumans().length && !subflowState.getPendingPermissions().length) { + result = subflowState.finalResponse(); + } + } else { + result = await execTool(agent.tools![toolCall.toolName], toolCall.arguments, { runId, signal, abortRegistry }); } - if (!subflowState.getPendingAskHumans().length && !subflowState.getPendingPermissions().length) { - result = subflowState.finalResponse(); + } catch (error) { + if ((error instanceof Error && error.name === "AbortError") || signal.aborted) { + throw error; } - } else { - result = await execTool(agent.tools![toolCall.toolName], toolCall.arguments, { runId, signal, abortRegistry }); + const message = error instanceof Error ? (error.message || error.name) : String(error); + _logger.log('tool failed', message); + result = { + success: false, + error: message, + toolName: toolCall.toolName, + }; } const resultPayload = result === undefined ? null : result; const resultMsg: z.infer = {