Fix tool errors not persisting

#3
by akseljoonas HF Staff - opened
backend/main.py CHANGED
@@ -12,7 +12,8 @@ from fastapi.staticfiles import StaticFiles
12
  from routes.agent import router as agent_router
13
  from routes.auth import router as auth_router
14
 
15
- load_dotenv()
 
16
 
17
  # Configure logging
18
  logging.basicConfig(
 
12
  from routes.agent import router as agent_router
13
  from routes.auth import router as auth_router
14
 
15
+ # Load .env from project root (parent directory)
16
+ load_dotenv(Path(__file__).parent.parent / ".env")
17
 
18
  # Configure logging
19
  logging.basicConfig(
backend/routes/agent.py CHANGED
@@ -206,12 +206,17 @@ async def create_session(
206
  Returns 503 if the server or user has reached the session limit.
207
  """
208
  # Extract the user's HF token (Bearer header or HttpOnly cookie)
 
209
  hf_token = None
210
  auth_header = request.headers.get("Authorization", "")
211
  if auth_header.startswith("Bearer "):
212
  hf_token = auth_header[7:]
213
  if not hf_token:
214
  hf_token = request.cookies.get("hf_access_token")
 
 
 
 
215
 
216
  try:
217
  session_id = await session_manager.create_session(
 
206
  Returns 503 if the server or user has reached the session limit.
207
  """
208
  # Extract the user's HF token (Bearer header or HttpOnly cookie)
209
+ # In dev mode, fall back to environment variable if no token in request
210
  hf_token = None
211
  auth_header = request.headers.get("Authorization", "")
212
  if auth_header.startswith("Bearer "):
213
  hf_token = auth_header[7:]
214
  if not hf_token:
215
  hf_token = request.cookies.get("hf_access_token")
216
+ if not hf_token and user["user_id"] == "dev":
217
+ # Dev mode: use HF_TOKEN from environment
218
+ import os
219
+ hf_token = os.environ.get("HF_TOKEN")
220
 
221
  try:
222
  session_id = await session_manager.create_session(
frontend/package-lock.json CHANGED
@@ -130,7 +130,6 @@
130
  "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
131
  "dev": true,
132
  "license": "MIT",
133
- "peer": true,
134
  "dependencies": {
135
  "@babel/code-frame": "^7.28.6",
136
  "@babel/generator": "^7.28.6",
@@ -447,7 +446,6 @@
447
  "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
448
  "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
449
  "license": "MIT",
450
- "peer": true,
451
  "dependencies": {
452
  "@babel/runtime": "^7.18.3",
453
  "@emotion/babel-plugin": "^11.13.5",
@@ -491,7 +489,6 @@
491
  "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz",
492
  "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==",
493
  "license": "MIT",
494
- "peer": true,
495
  "dependencies": {
496
  "@babel/runtime": "^7.18.3",
497
  "@emotion/babel-plugin": "^11.13.5",
@@ -1224,7 +1221,6 @@
1224
  "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.5.0.tgz",
1225
  "integrity": "sha512-yjvtXoFcrPLGtgKRxFaH6OQPtcLPhkloC0BML6rBG5UeldR0nPULR/2E2BfXdo5JNV7j7lOzrrLX2Qf/iSidow==",
1226
  "license": "MIT",
1227
- "peer": true,
1228
  "dependencies": {
1229
  "@babel/runtime": "^7.26.0",
1230
  "@mui/core-downloads-tracker": "^6.5.0",
@@ -1919,7 +1915,6 @@
1919
  "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
1920
  "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
1921
  "license": "MIT",
1922
- "peer": true,
1923
  "dependencies": {
1924
  "@types/prop-types": "*",
1925
  "csstype": "^3.2.2"
@@ -2005,7 +2000,6 @@
2005
  "integrity": "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==",
2006
  "dev": true,
2007
  "license": "MIT",
2008
- "peer": true,
2009
  "dependencies": {
2010
  "@typescript-eslint/scope-manager": "8.53.0",
2011
  "@typescript-eslint/types": "8.53.0",
@@ -2272,7 +2266,6 @@
2272
  "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
2273
  "dev": true,
2274
  "license": "MIT",
2275
- "peer": true,
2276
  "bin": {
2277
  "acorn": "bin/acorn"
2278
  },
@@ -2421,7 +2414,6 @@
2421
  }
2422
  ],
2423
  "license": "MIT",
2424
- "peer": true,
2425
  "dependencies": {
2426
  "baseline-browser-mapping": "^2.9.0",
2427
  "caniuse-lite": "^1.0.30001759",
@@ -2774,7 +2766,6 @@
2774
  "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
2775
  "dev": true,
2776
  "license": "MIT",
2777
- "peer": true,
2778
  "dependencies": {
2779
  "@eslint-community/eslint-utils": "^4.8.0",
2780
  "@eslint-community/regexpp": "^4.12.1",
@@ -4673,7 +4664,6 @@
4673
  "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
4674
  "dev": true,
4675
  "license": "MIT",
4676
- "peer": true,
4677
  "engines": {
4678
  "node": ">=12"
4679
  },
@@ -4771,7 +4761,6 @@
4771
  "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
4772
  "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
4773
  "license": "MIT",
4774
- "peer": true,
4775
  "dependencies": {
4776
  "loose-envify": "^1.1.0"
4777
  },
@@ -4784,7 +4773,6 @@
4784
  "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
4785
  "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
4786
  "license": "MIT",
4787
- "peer": true,
4788
  "dependencies": {
4789
  "loose-envify": "^1.1.0",
4790
  "scheduler": "^0.23.2"
@@ -5269,7 +5257,6 @@
5269
  "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
5270
  "dev": true,
5271
  "license": "Apache-2.0",
5272
- "peer": true,
5273
  "bin": {
5274
  "tsc": "bin/tsc",
5275
  "tsserver": "bin/tsserver"
@@ -5435,7 +5422,6 @@
5435
  "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
5436
  "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
5437
  "license": "MIT",
5438
- "peer": true,
5439
  "peerDependencies": {
5440
  "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
5441
  }
@@ -5474,7 +5460,6 @@
5474
  "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
5475
  "dev": true,
5476
  "license": "MIT",
5477
- "peer": true,
5478
  "dependencies": {
5479
  "esbuild": "^0.21.3",
5480
  "postcss": "^8.4.43",
 
130
  "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
131
  "dev": true,
132
  "license": "MIT",
 
133
  "dependencies": {
134
  "@babel/code-frame": "^7.28.6",
135
  "@babel/generator": "^7.28.6",
 
446
  "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
447
  "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
448
  "license": "MIT",
 
449
  "dependencies": {
450
  "@babel/runtime": "^7.18.3",
451
  "@emotion/babel-plugin": "^11.13.5",
 
489
  "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz",
490
  "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==",
491
  "license": "MIT",
 
492
  "dependencies": {
493
  "@babel/runtime": "^7.18.3",
494
  "@emotion/babel-plugin": "^11.13.5",
 
1221
  "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.5.0.tgz",
1222
  "integrity": "sha512-yjvtXoFcrPLGtgKRxFaH6OQPtcLPhkloC0BML6rBG5UeldR0nPULR/2E2BfXdo5JNV7j7lOzrrLX2Qf/iSidow==",
1223
  "license": "MIT",
 
1224
  "dependencies": {
1225
  "@babel/runtime": "^7.26.0",
1226
  "@mui/core-downloads-tracker": "^6.5.0",
 
1915
  "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
1916
  "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
1917
  "license": "MIT",
 
1918
  "dependencies": {
1919
  "@types/prop-types": "*",
1920
  "csstype": "^3.2.2"
 
2000
  "integrity": "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==",
2001
  "dev": true,
2002
  "license": "MIT",
 
2003
  "dependencies": {
2004
  "@typescript-eslint/scope-manager": "8.53.0",
2005
  "@typescript-eslint/types": "8.53.0",
 
2266
  "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
2267
  "dev": true,
2268
  "license": "MIT",
 
2269
  "bin": {
2270
  "acorn": "bin/acorn"
2271
  },
 
2414
  }
2415
  ],
2416
  "license": "MIT",
 
2417
  "dependencies": {
2418
  "baseline-browser-mapping": "^2.9.0",
2419
  "caniuse-lite": "^1.0.30001759",
 
2766
  "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
2767
  "dev": true,
2768
  "license": "MIT",
 
2769
  "dependencies": {
2770
  "@eslint-community/eslint-utils": "^4.8.0",
2771
  "@eslint-community/regexpp": "^4.12.1",
 
4664
  "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
4665
  "dev": true,
4666
  "license": "MIT",
 
4667
  "engines": {
4668
  "node": ">=12"
4669
  },
 
4761
  "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
4762
  "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
4763
  "license": "MIT",
 
4764
  "dependencies": {
4765
  "loose-envify": "^1.1.0"
4766
  },
 
4773
  "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
4774
  "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
4775
  "license": "MIT",
 
4776
  "dependencies": {
4777
  "loose-envify": "^1.1.0",
4778
  "scheduler": "^0.23.2"
 
5257
  "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
5258
  "dev": true,
5259
  "license": "Apache-2.0",
 
5260
  "bin": {
5261
  "tsc": "bin/tsc",
5262
  "tsserver": "bin/tsserver"
 
5422
  "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
5423
  "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
5424
  "license": "MIT",
 
5425
  "peerDependencies": {
5426
  "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
5427
  }
 
5460
  "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
5461
  "dev": true,
5462
  "license": "MIT",
 
5463
  "dependencies": {
5464
  "esbuild": "^0.21.3",
5465
  "postcss": "^8.4.43",
frontend/src/components/Chat/ToolCallGroup.tsx CHANGED
@@ -397,7 +397,7 @@ function InlineApproval({
397
  // ---------------------------------------------------------------------------
398
 
399
  export default function ToolCallGroup({ tools, approveTools }: ToolCallGroupProps) {
400
- const { setPanel, lockPanel, getJobUrl, getEditedScript } = useAgentStore();
401
  const researchSteps = useAgentStore(s => {
402
  const activeId = s.activeSessionId;
403
  return activeId ? (s.sessionStates[activeId]?.researchSteps) : undefined;
@@ -428,6 +428,35 @@ export default function ToolCallGroup({ tools, approveTools }: ToolCallGroupProp
428
  }
429
  }, [pendingTools, isSubmitting]);
430
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
431
  const { scriptLabelMap, toolDisplayMap } = useMemo(() => {
432
  const hfJobs = tools.filter(t => t.toolName === 'hf_jobs' && (t.input as Record<string, unknown>)?.script);
433
  const scriptMap: Record<string, string> = {};
@@ -655,21 +684,37 @@ export default function ToolCallGroup({ tools, approveTools }: ToolCallGroupProp
655
  const localDecision = decisions[tool.toolCallId];
656
 
657
  const cancelled = isCancelledTool(tool);
 
 
 
 
 
 
658
  const displayState = isPending && localDecision
659
  ? (localDecision.approved ? 'input-available' : 'output-denied')
660
  : state;
661
- const label = cancelled ? 'cancelled' : statusLabel(displayState as ToolPartState);
 
 
662
 
663
  // Parse job metadata from hf_jobs output and store
664
  const jobUrlFromStore = tool.toolName === 'hf_jobs' ? getJobUrl(tool.toolCallId) : undefined;
665
- const jobMetaFromOutput = tool.toolName === 'hf_jobs' && tool.state === 'output-available'
666
- ? parseJobMeta(tool.output)
 
 
667
  : {};
668
-
669
- // Combine job URL from store (available immediately) with output metadata (available at completion)
 
 
 
 
 
 
670
  const jobMeta = {
671
  jobUrl: jobUrlFromStore || jobMetaFromOutput.jobUrl,
672
- jobStatus: jobMetaFromOutput.jobStatus,
673
  };
674
 
675
  return (
@@ -691,9 +736,11 @@ export default function ToolCallGroup({ tools, approveTools }: ToolCallGroupProp
691
  <StatusIcon
692
  cancelled={cancelled}
693
  state={
694
- (tool.toolName === 'hf_jobs' && jobMeta.jobStatus && ['ERROR', 'FAILED', 'CANCELLED'].includes(jobMeta.jobStatus) && displayState === 'output-available')
695
  ? 'output-error'
696
- : displayState as ToolPartState
 
 
697
  }
698
  />
699
 
@@ -724,10 +771,12 @@ export default function ToolCallGroup({ tools, approveTools }: ToolCallGroupProp
724
  fontSize: '0.65rem',
725
  fontWeight: 600,
726
  bgcolor: cancelled ? 'rgba(255,255,255,0.05)'
727
- : displayState === 'output-error' ? 'rgba(224,90,79,0.12)'
728
  : displayState === 'output-denied' ? 'rgba(255,255,255,0.05)'
729
  : 'var(--accent-yellow-weak)',
730
- color: cancelled ? 'var(--muted-text)' : statusColor(displayState as ToolPartState),
 
 
731
  letterSpacing: '0.03em',
732
  }}
733
  />
 
397
  // ---------------------------------------------------------------------------
398
 
399
  export default function ToolCallGroup({ tools, approveTools }: ToolCallGroupProps) {
400
+ const { setPanel, lockPanel, getJobUrl, getEditedScript, setJobStatus, getJobStatus, setToolError, getToolError } = useAgentStore();
401
  const researchSteps = useAgentStore(s => {
402
  const activeId = s.activeSessionId;
403
  return activeId ? (s.sessionStates[activeId]?.researchSteps) : undefined;
 
428
  }
429
  }, [pendingTools, isSubmitting]);
430
 
431
+ // Clean up stale decisions for tools that are no longer pending
432
+ useEffect(() => {
433
+ const pendingIds = new Set(pendingTools.map(t => t.toolCallId));
434
+ const decisionIds = Object.keys(decisions);
435
+ const hasStale = decisionIds.some(id => !pendingIds.has(id));
436
+ if (hasStale) {
437
+ setDecisions(prev => {
438
+ const cleaned = { ...prev };
439
+ for (const id of decisionIds) {
440
+ if (!pendingIds.has(id)) delete cleaned[id];
441
+ }
442
+ return cleaned;
443
+ });
444
+ }
445
+ }, [pendingTools, decisions]);
446
+
447
+ // Persist error states when tools error
448
+ useEffect(() => {
449
+ for (const tool of tools) {
450
+ const currentlyHasError = tool.state === 'output-error';
451
+ const persistedError = getToolError(tool.toolCallId);
452
+
453
+ // Persist error state if we detect it and haven't already
454
+ if (currentlyHasError && !persistedError) {
455
+ setToolError(tool.toolCallId, true);
456
+ }
457
+ }
458
+ }, [tools, setToolError, getToolError]);
459
+
460
  const { scriptLabelMap, toolDisplayMap } = useMemo(() => {
461
  const hfJobs = tools.filter(t => t.toolName === 'hf_jobs' && (t.input as Record<string, unknown>)?.script);
462
  const scriptMap: Record<string, string> = {};
 
684
  const localDecision = decisions[tool.toolCallId];
685
 
686
  const cancelled = isCancelledTool(tool);
687
+ const currentlyHasError = state === 'output-error';
688
+ const persistedError = getToolError(tool.toolCallId);
689
+
690
+ // Use persisted error OR current error (persisting happens in useEffect)
691
+ const hasError = persistedError || currentlyHasError;
692
+
693
  const displayState = isPending && localDecision
694
  ? (localDecision.approved ? 'input-available' : 'output-denied')
695
  : state;
696
+ const label = cancelled ? 'cancelled'
697
+ : hasError ? 'error'
698
+ : statusLabel(displayState as ToolPartState);
699
 
700
  // Parse job metadata from hf_jobs output and store
701
  const jobUrlFromStore = tool.toolName === 'hf_jobs' ? getJobUrl(tool.toolCallId) : undefined;
702
+ const jobStatusFromStore = tool.toolName === 'hf_jobs' ? getJobStatus(tool.toolCallId) : undefined;
703
+
704
+ const jobMetaFromOutput = tool.toolName === 'hf_jobs' && (tool.output || (tool as Record<string, unknown>).errorText)
705
+ ? parseJobMeta(tool.output ?? (tool as Record<string, unknown>).errorText)
706
  : {};
707
+
708
+ // Store job status if we just parsed it and don't have it stored yet
709
+ if (tool.toolName === 'hf_jobs' && jobMetaFromOutput.jobStatus && !jobStatusFromStore) {
710
+ setJobStatus(tool.toolCallId, jobMetaFromOutput.jobStatus);
711
+ }
712
+
713
+ // Combine job URL and status from store (persisted) with output metadata (freshly parsed)
714
+ // Prefer stored values to ensure they persist across renders
715
  const jobMeta = {
716
  jobUrl: jobUrlFromStore || jobMetaFromOutput.jobUrl,
717
+ jobStatus: jobStatusFromStore || jobMetaFromOutput.jobStatus,
718
  };
719
 
720
  return (
 
736
  <StatusIcon
737
  cancelled={cancelled}
738
  state={
739
+ hasError
740
  ? 'output-error'
741
+ : ((tool.toolName === 'hf_jobs' && jobMeta.jobStatus && ['ERROR', 'FAILED', 'CANCELLED'].includes(jobMeta.jobStatus))
742
+ ? 'output-error'
743
+ : displayState as ToolPartState)
744
  }
745
  />
746
 
 
771
  fontSize: '0.65rem',
772
  fontWeight: 600,
773
  bgcolor: cancelled ? 'rgba(255,255,255,0.05)'
774
+ : hasError ? 'rgba(224,90,79,0.12)'
775
  : displayState === 'output-denied' ? 'rgba(255,255,255,0.05)'
776
  : 'var(--accent-yellow-weak)',
777
+ color: cancelled ? 'var(--muted-text)'
778
+ : hasError ? 'var(--accent-red)'
779
+ : statusColor(displayState as ToolPartState),
780
  letterSpacing: '0.03em',
781
  }}
782
  />
frontend/src/lib/sse-chat-transport.ts CHANGED
@@ -277,7 +277,8 @@ export class SSEChatTransport implements ChatTransport<UIMessage> {
277
  this.sessionId = sessionId;
278
  this.sideChannel = sideChannel;
279
  // Mark as connected immediately β€” no persistent connection to establish
280
- sideChannel.onConnectionChange(true);
 
281
  }
282
 
283
  updateSideChannel(sideChannel: SideChannelCallbacks): void {
 
277
  this.sessionId = sessionId;
278
  this.sideChannel = sideChannel;
279
  // Mark as connected immediately β€” no persistent connection to establish
280
+ // Defer to avoid setState during render
281
+ queueMicrotask(() => sideChannel.onConnectionChange(true));
282
  }
283
 
284
  updateSideChannel(sideChannel: SideChannelCallbacks): void {
frontend/src/store/agentStore.ts CHANGED
@@ -101,6 +101,12 @@ interface AgentStore {
101
  // Job URLs (tool_call_id -> job URL) for HF jobs
102
  jobUrls: Record<string, string>;
103
 
 
 
 
 
 
 
104
  // ── Per-session actions ─────────────────────────────────────────────
105
 
106
  /** Update a session's state. If it's the active session, also update flat state. */
@@ -138,6 +144,12 @@ interface AgentStore {
138
 
139
  setJobUrl: (toolCallId: string, jobUrl: string) => void;
140
  getJobUrl: (toolCallId: string) => string | undefined;
 
 
 
 
 
 
141
  }
142
 
143
  /**
@@ -159,6 +171,25 @@ function syncSnapshot(
159
  };
160
  }
161
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  export const useAgentStore = create<AgentStore>()((set, get) => ({
163
  sessionStates: {},
164
  activeSessionId: null,
@@ -178,6 +209,8 @@ export const useAgentStore = create<AgentStore>()((set, get) => ({
178
 
179
  editedScripts: {},
180
  jobUrls: {},
 
 
181
 
182
  // ── Per-session state management ──────────────────────────────────
183
 
@@ -349,4 +382,26 @@ export const useAgentStore = create<AgentStore>()((set, get) => ({
349
  },
350
 
351
  getJobUrl: (toolCallId) => get().jobUrls[toolCallId],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
  }));
 
101
  // Job URLs (tool_call_id -> job URL) for HF jobs
102
  jobUrls: Record<string, string>;
103
 
104
+ // Job statuses (tool_call_id -> job status) for HF jobs
105
+ jobStatuses: Record<string, string>;
106
+
107
+ // Tool error states (tool_call_id -> true if errored) - persisted across renders
108
+ toolErrors: Record<string, boolean>;
109
+
110
  // ── Per-session actions ─────────────────────────────────────────────
111
 
112
  /** Update a session's state. If it's the active session, also update flat state. */
 
144
 
145
  setJobUrl: (toolCallId: string, jobUrl: string) => void;
146
  getJobUrl: (toolCallId: string) => string | undefined;
147
+
148
+ setJobStatus: (toolCallId: string, status: string) => void;
149
+ getJobStatus: (toolCallId: string) => string | undefined;
150
+
151
+ setToolError: (toolCallId: string, hasError: boolean) => void;
152
+ getToolError: (toolCallId: string) => boolean | undefined;
153
  }
154
 
155
  /**
 
171
  };
172
  }
173
 
174
+ // Load persisted tool errors from localStorage
175
+ function loadToolErrors(): Record<string, boolean> {
176
+ try {
177
+ const stored = localStorage.getItem('hf-agent-tool-errors');
178
+ return stored ? JSON.parse(stored) : {};
179
+ } catch {
180
+ return {};
181
+ }
182
+ }
183
+
184
+ // Save tool errors to localStorage
185
+ function saveToolErrors(errors: Record<string, boolean>): void {
186
+ try {
187
+ localStorage.setItem('hf-agent-tool-errors', JSON.stringify(errors));
188
+ } catch (e) {
189
+ console.warn('Failed to persist tool errors:', e);
190
+ }
191
+ }
192
+
193
  export const useAgentStore = create<AgentStore>()((set, get) => ({
194
  sessionStates: {},
195
  activeSessionId: null,
 
209
 
210
  editedScripts: {},
211
  jobUrls: {},
212
+ jobStatuses: {},
213
+ toolErrors: loadToolErrors(),
214
 
215
  // ── Per-session state management ──────────────────────────────────
216
 
 
382
  },
383
 
384
  getJobUrl: (toolCallId) => get().jobUrls[toolCallId],
385
+
386
+ // ── Job Statuses ────────────────────────────────────────────────────
387
+
388
+ setJobStatus: (toolCallId, status) => {
389
+ set((state) => ({
390
+ jobStatuses: { ...state.jobStatuses, [toolCallId]: status },
391
+ }));
392
+ },
393
+
394
+ getJobStatus: (toolCallId) => get().jobStatuses[toolCallId],
395
+
396
+ // ── Tool Errors ─────────────────────────────────────────────────────
397
+
398
+ setToolError: (toolCallId, hasError) => {
399
+ set((state) => {
400
+ const updated = { ...state.toolErrors, [toolCallId]: hasError };
401
+ saveToolErrors(updated);
402
+ return { toolErrors: updated };
403
+ });
404
+ },
405
+
406
+ getToolError: (toolCallId) => get().toolErrors[toolCallId],
407
  }));
uv.lock CHANGED
The diff for this file is too large to render. See raw diff