diff --git a/app/desktop/src/platform/useDesktopEdgeEvents.ts b/app/desktop/src/platform/useDesktopEdgeEvents.ts index a27e8a46f..d5f58fec7 100644 --- a/app/desktop/src/platform/useDesktopEdgeEvents.ts +++ b/app/desktop/src/platform/useDesktopEdgeEvents.ts @@ -1,5 +1,6 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { normalizeEdgeEventsToTranscript } from '@shared/transcript'; +import { eventRunId } from '@shared/transcript/edgeEventEvidence'; import { getAgentActivityStore, type AgentActivityStatus } from '@shared/transcript/agentActivity'; import type { EventEnvelope } from '@shared/events'; import type { TranscriptBlock } from '@shared/transcript'; @@ -170,10 +171,6 @@ function eventThreadId(event: EventEnvelope): string | undefined { return stringField(event.payload.threadId) ?? stringField(event.scope.threadId); } -function eventRunId(event: EventEnvelope): string | undefined { - return stringField(event.payload.runId) ?? stringField(event.scope.runId); -} - function eventTimestampMs(event: EventEnvelope): number { const parsed = Date.parse(event.sentAt); return Number.isFinite(parsed) ? parsed : Number.POSITIVE_INFINITY; diff --git a/app/desktop/src/platform/useDesktopWorkbenchModel.ts b/app/desktop/src/platform/useDesktopWorkbenchModel.ts index d208be93d..19a18682b 100644 --- a/app/desktop/src/platform/useDesktopWorkbenchModel.ts +++ b/app/desktop/src/platform/useDesktopWorkbenchModel.ts @@ -9,6 +9,7 @@ import { type WorkbenchDataMode, } from '@shared/demo'; import { normalizeThreadItemsToTranscript } from '@shared/transcript'; +import { errorMessage } from '@shared/errors'; import { appDateLocaleTag } from '@shared/i18n/locale'; import type { TFunction } from 'i18next'; import { getI18n } from 'react-i18next'; @@ -613,9 +614,3 @@ function workspaceProjectToProjectInfo( feed: [], }; } - -function errorMessage(error: unknown, fallback: string): string { - if (error instanceof Error && error.message.trim()) return error.message; - if (typeof error === 'string' && error.trim()) return error; - return fallback; -} diff --git a/app/shared/src/errors.test.ts b/app/shared/src/errors.test.ts index 0bfed5232..f94a49cc3 100644 --- a/app/shared/src/errors.test.ts +++ b/app/shared/src/errors.test.ts @@ -1,12 +1,31 @@ import { describe, expect, it } from 'vitest'; import { AppError, + errorMessage, isErrorResponse, isTurnInProgressError, parseError, type ErrorBody, } from './errors'; +describe('errorMessage', () => { + it('prefers Error.message', () => { + expect(errorMessage(new Error('boom'), 'fallback')).toBe('boom'); + }); + + it('keeps a non-blank thrown string', () => { + expect(errorMessage('plain failure', 'fallback')).toBe('plain failure'); + }); + + it('falls back for blank, non-Error and empty values', () => { + expect(errorMessage('', 'fallback')).toBe('fallback'); + expect(errorMessage(' ', 'fallback')).toBe('fallback'); + expect(errorMessage(new Error(' '), 'fallback')).toBe('fallback'); + expect(errorMessage({ message: 'nope' }, 'fallback')).toBe('fallback'); + expect(errorMessage(undefined, 'fallback')).toBe('fallback'); + }); +}); + describe('isTurnInProgressError', () => { const turnBody: ErrorBody = { error: { code: 'turn_in_progress', message: 'agent instance already has an active task' }, diff --git a/app/shared/src/errors.ts b/app/shared/src/errors.ts index 6f7258a65..161291958 100644 --- a/app/shared/src/errors.ts +++ b/app/shared/src/errors.ts @@ -61,6 +61,20 @@ export function isTurnInProgressError(error: unknown): boolean { return error instanceof AppError && error.status === 409 && error.code === 'turn_in_progress'; } +/** + * Best-effort user-facing message for a caught value. + * + * Every platform had its own copy of this and one of them silently dropped the + * `string` case, so a thrown string surfaced as the generic fallback. This is + * the single definition: `Error.message`, then a non-blank thrown string, then + * the caller's fallback. + */ +export function errorMessage(error: unknown, fallback: string): string { + if (error instanceof Error && error.message.trim()) return error.message; + if (typeof error === 'string' && error.trim()) return error; + return fallback; +} + export async function parseError(response: Response): Promise { try { const body = await response.json(); diff --git a/app/web/src/App.tsx b/app/web/src/App.tsx index 4851e9858..309d98c48 100644 --- a/app/web/src/App.tsx +++ b/app/web/src/App.tsx @@ -3,6 +3,7 @@ import { useCallback, useMemo, useState, useSyncExternalStore } from 'react'; import { useTranslation } from 'react-i18next'; import { AgentHubWorkbench } from '@agenthub/workbench'; import { fetchAllPages } from '@shared/hub/paginate'; +import { errorMessage } from '@shared/errors'; import { webQueryKeys } from '@shared/stores/queryKeys'; import { CHATVIEW_I18N_NAMESPACE } from '@shared/chatview/i18n/resources'; import type { AgentConfig, ContactMember, ProjectDraft, ProjectInfo, SkillMarketItem, MCPMarketItem } from '@agenthub/workbench'; @@ -421,11 +422,6 @@ function WebWorkbenchRoot() { ); } -function errorMessage(error: unknown, fallback: string): string { - if (error instanceof Error && error.message.trim()) return error.message; - return fallback; -} - function normalizeHubSkillToMarketItem(raw: Record): SkillMarketItem { const item: SkillMarketItem = { id: String(raw.id ?? ''), diff --git a/app/web/src/platform/useWebWorkbenchModel.test.ts b/app/web/src/platform/useWebWorkbenchModel.test.ts index 5fb6b9109..f3cda392b 100644 --- a/app/web/src/platform/useWebWorkbenchModel.test.ts +++ b/app/web/src/platform/useWebWorkbenchModel.test.ts @@ -27,16 +27,6 @@ import { useWebSessionAutoMarkRead, } from './useWebWorkbenchModel'; import { webTranscript } from './webPlatform'; -import { errorMessage } from './webWorkbenchError'; - -describe('webWorkbenchError', () => { - it('prefers Error.message, then string, then fallback', () => { - expect(errorMessage(new Error('boom'), 'fallback')).toBe('boom'); - expect(errorMessage(' string-error ', 'fallback')).toBe(' string-error '); - expect(errorMessage(null, 'fallback')).toBe('fallback'); - expect(errorMessage(new Error(' '), 'fallback')).toBe('fallback'); - }); -}); describe('useWebWorkbenchModel helpers', () => { it('maps Hub contacts into shared workbench contacts', () => { diff --git a/app/web/src/platform/useWebWorkbenchModel.ts b/app/web/src/platform/useWebWorkbenchModel.ts index 672333249..2c910e419 100644 --- a/app/web/src/platform/useWebWorkbenchModel.ts +++ b/app/web/src/platform/useWebWorkbenchModel.ts @@ -88,7 +88,7 @@ import { resolveWebTranscriptMessages, resolveWebWorkbenchTranscript, } from './webWorkbenchTranscript'; -import { errorMessage } from './webWorkbenchError'; +import { errorMessage } from '@shared/errors'; const hubClient = createHubClient({ getToken: getAccessToken }); diff --git a/app/web/src/platform/webWorkbenchError.ts b/app/web/src/platform/webWorkbenchError.ts deleted file mode 100644 index 330bad225..000000000 --- a/app/web/src/platform/webWorkbenchError.ts +++ /dev/null @@ -1,5 +0,0 @@ -export function errorMessage(error: unknown, fallback: string): string { - if (error instanceof Error && error.message.trim()) return error.message; - if (typeof error === 'string' && error.trim()) return error; - return fallback; -} diff --git a/app/web/src/platform/webWorkbenchExecutionTargets.ts b/app/web/src/platform/webWorkbenchExecutionTargets.ts index 98b55f38a..2c52259df 100644 --- a/app/web/src/platform/webWorkbenchExecutionTargets.ts +++ b/app/web/src/platform/webWorkbenchExecutionTargets.ts @@ -2,7 +2,7 @@ import type { WorkbenchDataMode } from '@shared/demo'; import { isWorkbenchRealDataMode } from '@shared/demo'; import type { TranscriptBlock } from '@shared/transcript'; import type { ExecutionTargetInventoryItem } from '@/api/executionTargetQueries'; -import { errorMessage } from './webWorkbenchError'; +import { errorMessage } from '@shared/errors'; export type WebExecutionTargetStatusState = | 'hidden' diff --git a/app/web/src/platform/webWorkbenchHelpers.test.ts b/app/web/src/platform/webWorkbenchHelpers.test.ts index 305ec908a..405f52fab 100644 --- a/app/web/src/platform/webWorkbenchHelpers.test.ts +++ b/app/web/src/platform/webWorkbenchHelpers.test.ts @@ -1,14 +1,8 @@ import { describe, expect, it } from 'vitest'; -import { errorMessage } from './webWorkbenchError'; import { projectDraftToHubRequest } from './webWorkbenchProjects'; import { executionTargetLabel, resolveWebExecutionTargetStatus } from './webWorkbenchExecutionTargets'; describe('webWorkbench helper seam', () => { - it('errorMessage covers non-Error objects', () => { - expect(errorMessage({ message: 'nope' }, 'fallback')).toBe('fallback'); - expect(errorMessage('', 'fallback')).toBe('fallback'); - }); - it('projectDraftToHubRequest falls back to default name', () => { expect(projectDraftToHubRequest({ name: ' ', description: 'x' })).toEqual({ name: '未命名项目', diff --git a/app/web/src/platform/webWorkbenchProjects.ts b/app/web/src/platform/webWorkbenchProjects.ts index dcbe33af9..a59e69c0d 100644 --- a/app/web/src/platform/webWorkbenchProjects.ts +++ b/app/web/src/platform/webWorkbenchProjects.ts @@ -10,7 +10,7 @@ import type { WorkspaceProjectThread, WorkspaceProjectThreadMessage, } from '@/api/hubClient'; -import { errorMessage } from './webWorkbenchError'; +import { errorMessage } from '@shared/errors'; export function resolveWebWorkbenchProjects( projects: WorkspaceProject[] | undefined, diff --git a/app/web/src/platform/webWorkbenchTranscript.ts b/app/web/src/platform/webWorkbenchTranscript.ts index bbcc7adbb..4ec1fcd1a 100644 --- a/app/web/src/platform/webWorkbenchTranscript.ts +++ b/app/web/src/platform/webWorkbenchTranscript.ts @@ -17,7 +17,7 @@ import { } from '@shared/transcript'; import type { FileDiff } from '@shared/types/chat'; import { webHubEmptyTranscript } from './webPlatform'; -import { errorMessage } from './webWorkbenchError'; +import { errorMessage } from '@shared/errors'; export function resolveWebRuntimeEvidence(transcript: TranscriptBlock[]): RuntimeEvidenceSnapshot { const runId = resolveCurrentTranscriptRunId(transcript);