Skip to content

Commit ec5ecd5

Browse files
Juan Roldanclaude
andcommitted
fix: fetch_website timeout, version in navbar, chat text wall bug
Three independent, short fixes bundled: 1. fetch_website timeouts → 60s Bright Data web_unlocker1 needs 20–45s on hard Cloudflare targets and was timing out at 30s before clearance. The normal (non-unblocker) path was 15s which is tight for large pages behind proxies. Both now 60s with a one-line why-comment on the unblocker branch. 2. Surface granclaw version in the top navbar Vite injects __GRANCLAW_VERSION__ at build time from packages/cli/ package.json (no new endpoint, no runtime fetch). AppShell renders it as small muted mono text next to the wordmark so users can see which build is live without shelling into a container. 3. Chat wall-of-text when a turn has many tool calls The streaming handler was concatenating every text chunk into one string, so an assistant turn with N tool_use blocks interleaved with N text blocks rendered as one unreadable paragraph. Track the last streamed chunk type and prepend \n\n when switching from tool_call back to text, so markdown paints each block as its own paragraph. Skipped when the message is still empty to avoid a leading blank line on turns that start with tool calls. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 65ec5af commit ec5ecd5

4 files changed

Lines changed: 35 additions & 4 deletions

File tree

packages/backend/src/agent/runner-pi.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1093,7 +1093,8 @@ export async function runAgent(
10931093
'Authorization': `Bearer ${unblockerKey}`,
10941094
},
10951095
body: JSON.stringify({ zone: 'web_unlocker1', url: params.url, format: 'raw' }),
1096-
signal: AbortSignal.timeout(30_000),
1096+
// Bright Data needs 20–45s to solve Cloudflare challenges on hard targets.
1097+
signal: AbortSignal.timeout(60_000),
10971098
});
10981099
if (!res.ok) {
10991100
return { content: [{ type: 'text' as const, text: `fetch_website (unblocker): HTTP ${res.status} ${res.statusText}` }] };
@@ -1114,7 +1115,7 @@ export async function runAgent(
11141115
'User-Agent': 'Mozilla/5.0 (compatible; GranClaw/1.0; +https://granclaw.com)',
11151116
'Accept': 'text/html,application/xhtml+xml,*/*;q=0.9',
11161117
},
1117-
signal: AbortSignal.timeout(15_000),
1118+
signal: AbortSignal.timeout(60_000),
11181119
redirect: 'follow',
11191120
...(agentProxy ? { dispatcher: getProxyAgent(agentProxy) } : {}),
11201121
};

packages/frontend/src/components/AppShell.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
22

3+
declare const __GRANCLAW_VERSION__: string;
4+
35
export function AppShell() {
46
const location = useLocation();
57
const navigate = useNavigate();
@@ -17,6 +19,9 @@ export function AppShell() {
1719
<span className="font-display font-semibold text-on-surface tracking-tight truncate">
1820
GranClaw
1921
</span>
22+
<span className="text-xs font-mono text-on-surface/60 flex-shrink-0">
23+
v{__GRANCLAW_VERSION__}
24+
</span>
2025
</button>
2126
<span className="flex items-center gap-1.5 rounded-full bg-secondary-container/20 px-2 sm:px-3 py-1 text-xs font-mono text-secondary flex-shrink-0">
2227
<span className="h-1.5 w-1.5 rounded-full bg-secondary animate-pulse flex-shrink-0" />

packages/frontend/src/pages/ChatPage.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,13 +272,27 @@ export function ChatPage() {
272272
setIsSending(true);
273273

274274
let agentReply = '';
275+
// Track the last streamed chunk type so we can insert a paragraph
276+
// break between successive text blocks that are separated by tool
277+
// calls. Without this, the agent's narration between tool_use blocks
278+
// renders as one giant smashed-together paragraph (e.g. 53 tool
279+
// calls in a debug loop produces one unreadable wall of text).
280+
let lastChunkType: 'text' | 'tool_call' | null = null;
275281

276282
sendMessage(text, (chunk) => {
277283
if (chunk.type === 'text') {
278-
agentReply += chunk.text;
284+
// If the previous chunk in this turn was a tool_call, we're
285+
// starting a new text block — separate it from the previous
286+
// one with a blank line so markdown renders it as its own
287+
// paragraph. Skip the separator if the message is still empty
288+
// (leading tool calls with no prior text).
289+
const needsBreak = lastChunkType === 'tool_call' && agentReply.length > 0;
290+
const piece = needsBreak ? `\n\n${chunk.text}` : chunk.text;
291+
agentReply += piece;
279292
setMessages((prev) =>
280-
prev.map((m) => m.id === agentMsgId ? { ...m, text: m.text + chunk.text } : m)
293+
prev.map((m) => m.id === agentMsgId ? { ...m, text: m.text + piece } : m)
281294
);
295+
lastChunkType = 'text';
282296
} else if (chunk.type === 'tool_call') {
283297
setMessages((prev) =>
284298
prev.map((m) =>
@@ -287,6 +301,7 @@ export function ChatPage() {
287301
: m
288302
)
289303
);
304+
lastChunkType = 'tool_call';
290305
} else if (chunk.type === 'pending_approval') {
291306
setPendingApproval({ reason: chunk.reason });
292307
setMessages((prev) =>

packages/frontend/vite.config.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
11
import { defineConfig } from 'vite';
22
import react from '@vitejs/plugin-react';
3+
import { readFileSync } from 'node:fs';
4+
import { resolve } from 'node:path';
35

46
const backendPort = process.env.GRANCLAW_BACKEND_PORT ?? '3001';
57
const backendUrl = `http://localhost:${backendPort}`;
68
const backendWsUrl = `ws://localhost:${backendPort}`;
79

10+
const cliPkg = JSON.parse(
11+
readFileSync(resolve(__dirname, '../cli/package.json'), 'utf8')
12+
);
13+
const granclawVersion = cliPkg.version ?? '0.0.0';
14+
815
export default defineConfig({
916
plugins: [react()],
17+
define: {
18+
__GRANCLAW_VERSION__: JSON.stringify(granclawVersion),
19+
},
1020
server: {
1121
port: 5173,
1222
// Listen on all interfaces so the takeover URL (http://<lan-ip>:5173/...)

0 commit comments

Comments
 (0)