diff --git a/app/desktop/src/App.tsx b/app/desktop/src/App.tsx index 4ecd085de..e399b87c5 100644 --- a/app/desktop/src/App.tsx +++ b/app/desktop/src/App.tsx @@ -133,7 +133,7 @@ export default function App() { ); } -export interface DesktopWorkbenchAppProps { +interface DesktopWorkbenchAppProps { onLogout?: (() => void) | undefined; } diff --git a/app/desktop/src/__tests__/LoginForm.test.tsx b/app/desktop/src/__tests__/LoginForm.test.tsx index bb9a29d5d..92c00c932 100644 --- a/app/desktop/src/__tests__/LoginForm.test.tsx +++ b/app/desktop/src/__tests__/LoginForm.test.tsx @@ -65,27 +65,40 @@ describe('LoginForm', () => { }); }); - it('shows TokenDance errors from the auth hook', async () => { + // #2154 P2-10 (backported from web LoginForm): the banner must carry localized + // copy, never err.message. The test i18next instance echoes keys / honors + // defaultValue, so the assertions pin the resolved key instead of the raw + // transport string. + it('falls back to the localized generic failure instead of echoing err.message', async () => { mockLoginWithTokenDance.mockRejectedValueOnce(new Error('Hub login unavailable')); renderForm(); fireEvent.click(screen.getByRole('button', { name: 'auth.tokenDanceLogin' })); await waitFor(() => { - expect(screen.getByRole('alert')).toHaveTextContent('Hub login unavailable'); + const alert = screen.getByRole('alert'); + expect(alert).toHaveTextContent('auth.error.oidc.default'); + expect(alert.textContent).not.toContain('Hub login unavailable'); }); expect(screen.queryByRole('status')).not.toBeInTheDocument(); }); - it('falls back to the localized unavailable error when rejection has no message', async () => { - mockLoginWithTokenDance.mockRejectedValueOnce({}); + it('resolves known OidcError codes through auth.error.oidc.', async () => { + const { OidcError } = await import('@/api/hubAuth'); + mockLoginWithTokenDance.mockRejectedValueOnce( + new OidcError('startFailed', 'Failed to start OIDC login: fetch failed', 'fetch failed'), + ); renderForm(); fireEvent.click(screen.getByRole('button', { name: 'auth.tokenDanceLogin' })); await waitFor(() => { - expect(screen.getByRole('alert')).toHaveTextContent('auth.error.tokenDanceUnavailable'); + const alert = screen.getByRole('alert'); + expect(alert).toHaveTextContent('auth.error.oidc'); + expect(alert.textContent).not.toContain('fetch failed'); + expect(alert.textContent).not.toContain('Failed to start OIDC login'); }); + expect(screen.queryByRole('status')).not.toBeInTheDocument(); }); it('calls onSuccess and renders nothing when a Hub user is already authenticated', () => { diff --git a/app/desktop/src/api/agentProfileQueries.ts b/app/desktop/src/api/agentProfileQueries.ts index 766817d21..aa01b835f 100644 --- a/app/desktop/src/api/agentProfileQueries.ts +++ b/app/desktop/src/api/agentProfileQueries.ts @@ -149,37 +149,8 @@ export function useHubAgentProfiles(opts?: { enabled?: boolean }) { }); } -export function useHubCreateAgentProfile() { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: (data: import('@/api/hubClient').CreateAgentProfileRequest) => - getHubClient().createAgentProfile(data), - onSettled: () => { - queryClient.invalidateQueries({ queryKey: ['hub', 'agent-profiles'] }); - }, - }); -} -export function useHubUpdateAgentProfile() { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: ({ id, data }: { id: string; data: import('@/api/hubClient').UpdateAgentProfileRequest }) => - getHubClient().updateAgentProfile(id, data), - onSettled: () => { - queryClient.invalidateQueries({ queryKey: ['hub', 'agent-profiles'] }); - }, - }); -} -export function useHubDeleteAgentProfile() { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: (id: string) => getHubClient().deleteAgentProfile(id), - onSettled: () => { - queryClient.invalidateQueries({ queryKey: ['hub', 'agent-profiles'] }); - }, - }); -} /** Safely parse a JSON string field from Hub, returning undefined on failure. */ function safeJsonParse(value: string | undefined | null): T | undefined { diff --git a/app/desktop/src/api/edgeClient.ts b/app/desktop/src/api/edgeClient.ts index 15e379721..24b444afe 100644 --- a/app/desktop/src/api/edgeClient.ts +++ b/app/desktop/src/api/edgeClient.ts @@ -257,23 +257,7 @@ export async function fetchThreadPins(threadId: string): Promise>(listResponseSchema(ThreadPinInfoSchema), unwrapEdgeResponse(await res.json()), 'threadPins'); } -export async function pinThreadItem(threadId: string, itemId: string, pinnedBy?: string): Promise { - const res = await edgeFetch(`${BASE}/v1/threads/${encodeURIComponent(threadId)}/pins`, { - method: 'POST', - ...edgeDevRequestInit({}, { 'Content-Type': 'application/json' }), - body: JSON.stringify({ itemId, pinnedBy }), - }); - if (!res.ok) throw await parseError(res); - return safeParse(ThreadPinInfoSchema, unwrapEdgeResponse(await res.json()), 'pinThreadItem'); -} -export async function deleteThreadPin(threadId: string, itemId: string): Promise { - const res = await edgeFetch(`${BASE}/v1/threads/${encodeURIComponent(threadId)}/pins?itemId=${encodeURIComponent(itemId)}`, { - method: 'DELETE', - ...edgeDevRequestInit(), - }); - if (!res.ok) throw await parseError(res); -} export async function fetchRuns(projectId?: string, threadId?: string): Promise> { const params = new URLSearchParams(); @@ -312,14 +296,14 @@ export async function fetchRunDiff(runId: string): Promise { return safeParse(RunDiffSchema, unwrapEdgeResponse(await res.json()), 'runDiff'); } -export interface ApplyRunDiffRequest { +interface ApplyRunDiffRequest { filePath: string; hunkIndex: number; accepted: boolean; workDir: string; } -export interface ApplyRunDiffResponse { +interface ApplyRunDiffResponse { runId: string; filePath: string; hunkIndex: number; @@ -327,12 +311,12 @@ export interface ApplyRunDiffResponse { applied: boolean; } -export interface ApplyAllRunDiffsRequest { +interface ApplyAllRunDiffsRequest { decisions: Array>; workDir: string; } -export interface ApplyAllRunDiffsResponse { +interface ApplyAllRunDiffsResponse { runId: string; applied: number; } diff --git a/app/desktop/src/api/eventClient.ts b/app/desktop/src/api/eventClient.ts index 2cc733c3a..d83047391 100644 --- a/app/desktop/src/api/eventClient.ts +++ b/app/desktop/src/api/eventClient.ts @@ -23,7 +23,7 @@ export type { } from '@agenthub/shared'; export type { EventEnvelope } from '@shared/events'; -export interface EventStreamLegacyOptions extends EventStreamOptions { +interface EventStreamLegacyOptions extends EventStreamOptions { /** * When true, also append access_token to the WS URL (legacy fallback). * Default false — Edge rejects query access_token; prefer diff --git a/app/desktop/src/api/executionTargetQueries.ts b/app/desktop/src/api/executionTargetQueries.ts index c3b5fe5e6..c70f62ef6 100644 --- a/app/desktop/src/api/executionTargetQueries.ts +++ b/app/desktop/src/api/executionTargetQueries.ts @@ -38,7 +38,7 @@ export interface ExecutionTargetInventorySummary { byType: Record; } -export interface SyncLocalEdgeExecutionTargetInput { +interface SyncLocalEdgeExecutionTargetInput { deviceId: string; localEdgeTarget: DesktopExecutionTarget; registeredTargetId?: string; @@ -158,7 +158,7 @@ export function summarizeExecutionTargets(targets: ExecutionTargetInventoryItem[ for (const target of targets) { byType[String(target.target_type) as ExecutionTargetType] = (byType[String(target.target_type) as ExecutionTargetType] ?? 0) + 1; if (target.is_online) online += 1; - if (target.health_state === 'healthy') healthy += 1; + if (target.health_state === 'healthy' || target.health_state === 'online') healthy += 1; else if (target.health_state === 'degraded') degraded += 1; else if (target.health_state === 'offline') offline += 1; else unknown += 1; diff --git a/app/desktop/src/api/hubClient.ts b/app/desktop/src/api/hubClient.ts index cc0f8f975..2f5aec0a3 100644 --- a/app/desktop/src/api/hubClient.ts +++ b/app/desktop/src/api/hubClient.ts @@ -230,7 +230,7 @@ export function createHubClient(opts: HubClientOptions = {}): SharedHubClient { // exactOptionalPropertyTypes forbids assigning `undefined` to optional string // fields, so optional top-level fields are only set when defined. The shared // builder serializes this object verbatim, so the wire shape is unchanged. - const toSharedTarget = (data: CreateExecutionTargetRequest | UpdateExecutionTargetRequest): HubExecutionTargetRequest => { + const toSharedTarget = (data: Partial): HubExecutionTargetRequest => { const target: HubExecutionTargetRequest = { name: data.name ?? '', type: (data.target_type as ExecutionTargetType | undefined) ?? 'local_edge', @@ -261,9 +261,9 @@ export function createHubClient(opts: HubClientOptions = {}): SharedHubClient { return { ...client, - createExecutionTarget: (data: CreateExecutionTargetRequest) => + createExecutionTarget: (data: HubExecutionTargetRequest) => client.createExecutionTarget(toSharedTarget(data)), - updateExecutionTarget: (id: string, data: UpdateExecutionTargetRequest) => + updateExecutionTarget: (id: string, data: Partial) => client.updateExecutionTarget(id, toSharedTarget(data)), }; } @@ -272,8 +272,8 @@ export function createHubClient(opts: HubClientOptions = {}): SharedHubClient { // Prefer Hub* / shared types for new code. These keep existing imports compiling // while execution-target inventory still uses richer desktop fields. -export type ExecutionTargetHealthState = 'unknown' | 'healthy' | 'degraded' | 'offline' | string; -export type ExecutionTargetTrustLevel = 'local' | 'remote' | 'cloud' | 'relay' | string; +export type ExecutionTargetHealthState = 'unknown' | 'online' | 'healthy' | 'degraded' | 'offline' | 'stale' | 'mismatch' | 'registered'; +export type ExecutionTargetTrustLevel = 'local' | 'remote' | 'cloud' | 'relay'; export interface CreateExecutionTargetRequest { name: string; @@ -289,7 +289,6 @@ export interface CreateExecutionTargetRequest { auth_method?: 'none' | 'ssh_tunnel' | 'tailscale_mtls' | 'hub_jwt' | string; } -export type UpdateExecutionTargetRequest = Partial; export interface ExecutionTargetListResponse { items: ExecutionTarget[]; diff --git a/app/desktop/src/api/hubQueries.ts b/app/desktop/src/api/hubQueries.ts index cf174ae78..5261a6e70 100644 --- a/app/desktop/src/api/hubQueries.ts +++ b/app/desktop/src/api/hubQueries.ts @@ -110,10 +110,6 @@ export function useHubCreateContactGroup() { // ── Workspace Projects ──────────────────────────────────────────── -export interface WorkspaceProjectPage { - items: Awaited['listWorkspaceProjects']>>['items']; - nextPageCursor?: string; -} export function useHubWorkspaceProjects(opts?: { enabled?: boolean }) { return useQuery({ diff --git a/app/desktop/src/api/runEvidenceQueries.ts b/app/desktop/src/api/runEvidenceQueries.ts index b8c3d81a2..dbf6a8c77 100644 --- a/app/desktop/src/api/runEvidenceQueries.ts +++ b/app/desktop/src/api/runEvidenceQueries.ts @@ -6,7 +6,7 @@ import type { Artifact, Preview, RunDiff } from '@shared/types'; import type { FileDiff } from '@shared/types/chat'; import { fetchArtifacts, fetchPreviews, fetchRunDiff } from './edgeClient'; -export interface RunEvidenceState { +interface RunEvidenceState { diffs: FileDiff[]; artifacts: Artifact[]; previews: Preview[]; @@ -50,7 +50,7 @@ function fallbackDiff(file: RunDiff['files'][number]): FileDiff { }; } -export function toReviewDiffs(runDiff: RunDiff | undefined): FileDiff[] { +function toReviewDiffs(runDiff: RunDiff | undefined): FileDiff[] { if (!runDiff) return []; return runDiff.files.flatMap((file) => { const parsed = parseUnifiedDiff(file.diff, file.path) as FileDiff[]; diff --git a/app/desktop/src/api/runQueries.ts b/app/desktop/src/api/runQueries.ts index bbb5105c0..949a45fc4 100644 --- a/app/desktop/src/api/runQueries.ts +++ b/app/desktop/src/api/runQueries.ts @@ -33,7 +33,7 @@ export function findActiveEdgeRun(runs: RunInfo[] | undefined): RunInfo | undefi type RunQuerySnapshot = Array<[readonly unknown[], ListResponse | undefined]>; -export function snapshotRunQueries(qc: QueryClient): RunQuerySnapshot { +function snapshotRunQueries(qc: QueryClient): RunQuerySnapshot { return qc.getQueriesData>({ queryKey: edgeQueryKeys.runs.root }); } @@ -50,7 +50,7 @@ export function upsertRunInQueries(qc: QueryClient, run: RunInfo) { }); } -export function restoreRunsSnapshot(qc: QueryClient, snapshot: RunQuerySnapshot | undefined) { +function restoreRunsSnapshot(qc: QueryClient, snapshot: RunQuerySnapshot | undefined) { if (!snapshot) return; for (const [queryKey, value] of snapshot) { qc.setQueryData(queryKey, value); diff --git a/app/desktop/src/api/schemas.ts b/app/desktop/src/api/schemas.ts index 09c05de9e..24f530279 100644 --- a/app/desktop/src/api/schemas.ts +++ b/app/desktop/src/api/schemas.ts @@ -31,7 +31,7 @@ export const RunnerSchema = z.object({ // ── Agent ─────────────────────────────────────── -export const AgentCapabilitiesSchema = z.object({ +const AgentCapabilitiesSchema = z.object({ streaming: z.boolean(), toolCalls: z.boolean(), fileChanges: z.boolean(), @@ -63,7 +63,7 @@ export const AgentInfoSchema = z.object({ // ── Model catalog ─────────────────────────────── -export const ModelCatalogItemSchema = z.object({ +const ModelCatalogItemSchema = z.object({ id: z.string(), value: z.string(), label: z.string(), @@ -79,7 +79,7 @@ export const ModelCatalogItemSchema = z.object({ default: z.boolean().optional(), }); -export const ModelCatalogSourceSchema = z.object({ +const ModelCatalogSourceSchema = z.object({ id: z.string(), label: z.string(), status: z.enum(['ready', 'configured', 'unavailable']).or(z.string()), @@ -93,7 +93,7 @@ export const ModelCatalogResponseSchema = z.object({ // ── Page / List ───────────────────────────────── -export const PageInfoSchema = z.object({ +const PageInfoSchema = z.object({ nextCursor: z.string().optional(), hasMore: z.boolean(), }); @@ -162,7 +162,7 @@ export const RunInfoSchema = z.object({ * Parse an API response with a Zod schema. On failure, logs a warning * and returns the raw data (never throws), so schema drift cannot white-screen the UI. */ -export const RunDiffFileSchema = z.object({ +const RunDiffFileSchema = z.object({ path: z.string(), diff: z.string(), status: z.enum(['added', 'modified', 'deleted']), @@ -190,7 +190,7 @@ export const ApplyAllRunDiffsResponseSchema = z.object({ // GET /v1/runs/{runId}/checkpoint — pre-run checkpoint inventory (#1968). // Read-only evidence; no content and no restore/write-back surface. -export const RunCheckpointFileEntrySchema = z.object({ +const RunCheckpointFileEntrySchema = z.object({ path: z.string(), sizeBytes: z.number(), hash: z.string(), diff --git a/app/desktop/src/components/DesktopChrome.tsx b/app/desktop/src/components/DesktopChrome.tsx index c47002172..3495e42dc 100644 --- a/app/desktop/src/components/DesktopChrome.tsx +++ b/app/desktop/src/components/DesktopChrome.tsx @@ -12,7 +12,7 @@ import styles from './DesktopChrome.module.css'; type WindowCommand = 'minimize' | 'toggleMaximize' | 'close'; -export interface DesktopChromeProps { +interface DesktopChromeProps { children: ReactNode; showNavigationControls?: boolean | undefined; } diff --git a/app/desktop/src/components/DesktopEntryGate.tsx b/app/desktop/src/components/DesktopEntryGate.tsx index 5ea983f8a..0b4078a60 100644 --- a/app/desktop/src/components/DesktopEntryGate.tsx +++ b/app/desktop/src/components/DesktopEntryGate.tsx @@ -7,7 +7,7 @@ import agentHubLogo from '@/assets/agenthub-product-icon-rounded.svg'; import tokenDanceLogo from '@/assets/tokendance-product-mark-transparent.svg'; import styles from './DesktopEntryGate.module.css'; -export interface DesktopEntryGateProps { +interface DesktopEntryGateProps { onLoginSuccess: () => void; onContinueDemo: () => void; onConnectEdge: () => void; diff --git a/app/desktop/src/components/LoginForm.tsx b/app/desktop/src/components/LoginForm.tsx index a45ad90bb..95baf77fe 100644 --- a/app/desktop/src/components/LoginForm.tsx +++ b/app/desktop/src/components/LoginForm.tsx @@ -30,11 +30,19 @@ export default function LoginForm({ onSuccess }: LoginFormProps) { await loginWithTokenDance(); setIdentityNotice(t('auth.tokenDanceCallbackPending')); } catch (err: unknown) { + // #2154 P2-10: never render err.message. The OIDC failures are produced + // in English at the transport layer ("Failed to start OIDC login: fetch + // failed"), so echoing them put a raw technical string in a localized + // login screen. Same mapping as web's LoginForm: known codes resolve + // through auth.error.oidc., everything else falls back to the + // generic localized failure. if (err instanceof OidcError) { - setServerError(t(`auth.error.oidc.${err.code}` as const, { detail: err.detail ?? '', defaultValue: 'Login failed' })); + setServerError(t(`auth.error.oidc.${err.code}`, { + detail: err.detail ?? '', + defaultValue: t('auth.error.oidc.default'), + })); } else { - const message = err instanceof Error ? err.message : ''; - setServerError(message || t('auth.error.tokenDanceUnavailable')); + setServerError(t('auth.error.oidc.default')); } } finally { setIdentityLoading(false); diff --git a/app/desktop/src/components/OnboardingOverlay.tsx b/app/desktop/src/components/OnboardingOverlay.tsx index 75420d32f..2fb6e4b0d 100644 --- a/app/desktop/src/components/OnboardingOverlay.tsx +++ b/app/desktop/src/components/OnboardingOverlay.tsx @@ -14,7 +14,7 @@ const ONBOARDING_STEPS: OnboardingStepDescriptor[] = [ { titleKey: 'onboarding.step2.title', bodyKey: 'onboarding.step2.body' }, ]; -export interface OnboardingOverlayProps { +interface OnboardingOverlayProps { /** Called once when the user finishes the last step or skips. */ onFinish: () => void; } diff --git a/app/desktop/src/hooks/hubIntegrationMappers.ts b/app/desktop/src/hooks/hubIntegrationMappers.ts index 46c5bfd32..3e772749f 100644 --- a/app/desktop/src/hooks/hubIntegrationMappers.ts +++ b/app/desktop/src/hooks/hubIntegrationMappers.ts @@ -14,7 +14,7 @@ import { parseStringRecord, } from './hubIntegrationParseHelpers'; -export interface TeamRouteContext { +interface TeamRouteContext { teamId: string; teamRunId: string; teamMemberRole?: string; @@ -32,7 +32,7 @@ export interface HubDispatchTarget { deviceId: string; } -export interface DispatchTargetBindingEvidence { +interface DispatchTargetBindingEvidence { expectedTargetId: string; observedTargetId?: string; expectedEdgeDeviceId: string; @@ -332,7 +332,7 @@ export const FINAL_OUTPUT_MAX_CHARS = 32_000; * - `data`: the unwrapped dispatch payload (parsed from inner `payload` string for relay frames, or the raw frame for direct dispatches) * - `relayCommandId`: the relay command id when isRelay is true */ -export interface RelayFrameParseResult { +interface RelayFrameParseResult { isRelay: boolean; data: Record; relayCommandId: string | null; diff --git a/app/desktop/src/hooks/useDeviceRegistration.ts b/app/desktop/src/hooks/useDeviceRegistration.ts index 49cbf3b6c..151d5acae 100644 --- a/app/desktop/src/hooks/useDeviceRegistration.ts +++ b/app/desktop/src/hooks/useDeviceRegistration.ts @@ -8,11 +8,11 @@ import { APP_VERSION } from '@/config'; import type { HubClient } from '@/api/hubClient'; import { getOrCreateDeviceId } from '@shared/api/deviceId'; -export const DESKTOP_DEVICE_CAPABILITIES = ['local_edge', 'agent.dispatch', 'agent.control']; +const DESKTOP_DEVICE_CAPABILITIES = ['local_edge', 'agent.dispatch', 'agent.control']; -export type DeviceRegistrationStatus = 'idle' | 'registering' | 'registered' | 'error'; +type DeviceRegistrationStatus = 'idle' | 'registering' | 'registered' | 'error'; -export interface DeviceRegistrationState { +interface DeviceRegistrationState { deviceId: string | null; status: DeviceRegistrationStatus; error: string | null; diff --git a/app/desktop/src/hooks/useHealth.ts b/app/desktop/src/hooks/useHealth.ts index e2257fbc9..74089affa 100644 --- a/app/desktop/src/hooks/useHealth.ts +++ b/app/desktop/src/hooks/useHealth.ts @@ -5,14 +5,14 @@ import { fetchHealth } from '@/api/edgeClient'; import type { HealthResponse } from '@shared/types'; import { HEALTH_POLL_MS } from '@/config'; -export interface HealthState { +interface HealthState { online: boolean; health: HealthResponse | null; lastError: string | null; refetch: () => void; } -export interface UseHealthOptions { +interface UseHealthOptions { enabled?: boolean; } diff --git a/app/desktop/src/hooks/useHubEventStream.ts b/app/desktop/src/hooks/useHubEventStream.ts index 7153e5d3a..5496fa00b 100644 --- a/app/desktop/src/hooks/useHubEventStream.ts +++ b/app/desktop/src/hooks/useHubEventStream.ts @@ -28,7 +28,7 @@ import { hubQueryKeys } from '@shared/stores/queryKeys'; // ── Public types ───────────────────────────────── -export interface HubEventStreamState { +interface HubEventStreamState { status: TransportStatus; lastFrame: HubFrame | null; lastMessage: HubMessage | null; @@ -37,7 +37,7 @@ export interface HubEventStreamState { onlineUsers: string[]; } -export interface HubEventStreamHandle extends HubEventStreamState { +interface HubEventStreamHandle extends HubEventStreamState { /** The underlying Hub WS handle for lower-level consumers. */ hubWS: HubWSHandle | null; /** Send a typing indicator for a session. */ diff --git a/app/desktop/src/hooks/useHubIntegration.ts b/app/desktop/src/hooks/useHubIntegration.ts index a82393b51..8fbf621a4 100644 --- a/app/desktop/src/hooks/useHubIntegration.ts +++ b/app/desktop/src/hooks/useHubIntegration.ts @@ -50,7 +50,7 @@ export { useTaskBridgeStore }; // ── Options ───────────────────────────────────────── -export interface HubIntegrationOptions { +interface HubIntegrationOptions { /** Hub WebSocket handle (already connected & authenticated). Null disables the bridge. */ hubWS: HubWSHandle | null; /** Hub REST client for reporting task progress. */ @@ -63,7 +63,7 @@ export interface HubIntegrationOptions { onDispatch?: (task: AgentTask) => void; } -export interface HubIntegrationHandle { +interface HubIntegrationHandle { /** All bridged tasks (queued + running + done + failed). */ tasks: AgentTask[]; /** Number of currently active (running) tasks. */ diff --git a/app/desktop/src/platform/desktopPlatform.ts b/app/desktop/src/platform/desktopPlatform.ts index f83a2971e..9d730f511 100644 --- a/app/desktop/src/platform/desktopPlatform.ts +++ b/app/desktop/src/platform/desktopPlatform.ts @@ -3,10 +3,7 @@ import { formatComposerPromptWithContext } from '@shared/composer'; import type { AttachmentRef, ComposerIntent, ComposerSubmitResult } from '@shared/composer'; import { computeFileHash } from '@shared/composer'; import { - WORKBENCH_DEMO_FALLBACK_CONVERSATION_ID, - demoWorkbenchAgents, isWorkbenchFixtureDataMode, - resolveDemoWorkbenchTranscript, resolveWorkbenchDataMode, workbenchDemoRuntimeStore, } from '@shared/demo'; @@ -17,11 +14,9 @@ import { type AgentHubPlatform, type LocalCliDiscoveryManifest, type RuntimeSessionSummary, - type WorkbenchAgent, type WorkbenchConversation, } from '@shared/platform'; import type { EvidenceRef } from '@shared/transcript'; -import type { TranscriptBlock } from '@shared/transcript'; import type { RunInfo, StartRunRequest } from '@shared/types'; import { createHubClient } from '@/api/hubClient'; import { applyAllRunDiffs, applyRunDiff, fetchRunCheckpoint, fetchRunCheckpointFile } from '@/api/edgeClient'; @@ -42,19 +37,9 @@ import { import { resolveDesktopTargetPreference, type DesktopTargetPreference } from './targetPreference'; import { createDesktopSettingsAdapter } from './desktopSettingsAdapter'; -export const DESKTOP_FALLBACK_CONVERSATION_ID = WORKBENCH_DEMO_FALLBACK_CONVERSATION_ID; -export const desktopConversations: WorkbenchConversation[] = - workbenchDemoRuntimeStore.getSnapshot().conversations; -export const desktopAgents: WorkbenchAgent[] = demoWorkbenchAgents; -export const desktopTranscript: TranscriptBlock[] = resolveDemoWorkbenchTranscript( - DESKTOP_FALLBACK_CONVERSATION_ID, -); -export function resolveDesktopPreviewTranscript(conversationId: string): TranscriptBlock[] { - return workbenchDemoRuntimeStore.resolveTranscript(conversationId); -} -export interface DesktopPlatformOptions { +interface DesktopPlatformOptions { activeProjectId?: string; getEdgeHostReadiness?: () => Promise; getLocalEdgeDiagnostics?: () => Promise; @@ -66,7 +51,7 @@ export interface DesktopPlatformOptions { demoRuntimeFallback?: boolean; } -export interface DesktopEdgeHostReadiness { +interface DesktopEdgeHostReadiness { running: boolean; pid: number | null; port: number; @@ -95,7 +80,7 @@ export interface DesktopEdgeHostReadiness { direct_cli_spawn: false; } -export interface DesktopLocalEdgeDiagnostics { +interface DesktopLocalEdgeDiagnostics { readiness: DesktopEdgeHostReadiness; status: { running: boolean; @@ -130,7 +115,7 @@ export interface DesktopLocalEdgeDiagnostics { }; } -export interface DesktopHostPort { +interface DesktopHostPort { executionTargetPreference(): DesktopTargetPreference; edgeHostReadiness(): Promise; localEdgeDiagnostics(): Promise; @@ -138,7 +123,7 @@ export interface DesktopHostPort { listRuntimeSessions(limit?: number): Promise; } -export interface DesktopPlatform extends AgentHubPlatform { +interface DesktopPlatform extends AgentHubPlatform { host: DesktopHostPort; } @@ -287,16 +272,16 @@ function readEdgeHostReadiness(): Promise { return invoke('get_edge_host_readiness'); } -export function readLocalEdgeDiagnostics(): Promise { +function readLocalEdgeDiagnostics(): Promise { return invoke('get_local_edge_diagnostics'); } -export function readLocalCliDiscovery(): Promise { +function readLocalCliDiscovery(): Promise { return invoke('get_local_cli_discovery'); } /** Desktop host: Edge GET /v1/runtime-sessions via typed fetch (no foreign store). */ -export async function readRuntimeSessions(limit = 50): Promise { +async function readRuntimeSessions(limit = 50): Promise { // Data-mode gate (#1995): the workbench chrome calls this port on mount // (entry preflight). Pinned mock/fixture surfaces must not contact the // Local Edge, so they receive an honest empty import list instead of a diff --git a/app/desktop/src/platform/desktopRuntimeSessions.ts b/app/desktop/src/platform/desktopRuntimeSessions.ts index ed42653cf..1035502a8 100644 --- a/app/desktop/src/platform/desktopRuntimeSessions.ts +++ b/app/desktop/src/platform/desktopRuntimeSessions.ts @@ -5,7 +5,7 @@ import type { RuntimeSessionImportItem } from '@agenthub/workbench'; * The Edge REST path lives on the Desktop platform adapter only — shared * code consumes this data through `HostDiagnosticsPort.listRuntimeSessions`. */ -export type FetchDesktopRuntimeSessionsOptions = { +type FetchDesktopRuntimeSessionsOptions = { edgeBaseUrl: string; limit?: number; fetchImpl?: typeof fetch; diff --git a/app/desktop/src/platform/edgeCapabilityMapper.ts b/app/desktop/src/platform/edgeCapabilityMapper.ts index 96f49a25a..a0d072234 100644 --- a/app/desktop/src/platform/edgeCapabilityMapper.ts +++ b/app/desktop/src/platform/edgeCapabilityMapper.ts @@ -37,7 +37,7 @@ export interface DesktopExecutionTarget { capabilityIds: string[]; } -export type DesktopEdgeDispatchDisabledReason = +type DesktopEdgeDispatchDisabledReason = | 'signed-out' | 'hub-targets-loading' | 'hub-targets-error' @@ -53,7 +53,7 @@ export type DesktopEdgeDispatchDisabledReason = | 'local-edge-health-unknown' | 'host-preflight-blocked'; -export interface DesktopEdgeRegisteredTargetSnapshot { +interface DesktopEdgeRegisteredTargetSnapshot { id: string; name?: string; device_id?: string | null; @@ -62,7 +62,7 @@ export interface DesktopEdgeRegisteredTargetSnapshot { is_online?: boolean; } -export interface DesktopEdgeHostReadinessSnapshot { +interface DesktopEdgeHostReadinessSnapshot { health_url?: string; store_db_policy?: string; log_paths?: { @@ -77,7 +77,7 @@ export interface DesktopEdgeHostReadinessSnapshot { direct_cli_spawn?: boolean; } -export interface DesktopEdgeDispatchReadinessInput { +interface DesktopEdgeDispatchReadinessInput { hubSessionActive: boolean; deviceId?: string | null; edgeOnline: boolean; @@ -89,7 +89,7 @@ export interface DesktopEdgeDispatchReadinessInput { hostReadiness?: DesktopEdgeHostReadinessSnapshot | null; } -export interface DesktopEdgeDispatchReadiness { +interface DesktopEdgeDispatchReadiness { dispatchReady: boolean; disabledReason: DesktopEdgeDispatchDisabledReason | null; dispatchTarget: { targetId: string; deviceId: string } | null; @@ -114,7 +114,7 @@ export interface DesktopEdgeDispatchReadiness { directCliSpawn: false; } -export interface DesktopEdgeTargetBindingEvidence { +interface DesktopEdgeTargetBindingEvidence { expectedTargetId: string | null; observedTargetId: string | null; expectedEdgeDeviceId: string | null; diff --git a/app/desktop/src/stores/hubEventBridge.ts b/app/desktop/src/stores/hubEventBridge.ts index 7bc53e019..cac491048 100644 --- a/app/desktop/src/stores/hubEventBridge.ts +++ b/app/desktop/src/stores/hubEventBridge.ts @@ -4,7 +4,7 @@ // real-time state sync. // // The Desktop has two WS connections: -// 1. Edge local event stream (edgeEventBridge.ts) — runtime events +// 1. Edge local event stream (platform/useDesktopEdgeEvents.ts + api/eventClient.ts, createEventStream) — runtime events // 2. Hub WS (this bridge) — team/agent/IM dispatch events from the Hub import type { QueryClient } from '@tanstack/react-query'; @@ -420,7 +420,7 @@ export interface DesktopHubWSLike { * Wire Desktop Hub WS events to React Query cache invalidation and * Zustand store updates. Returns a handle with a `destroy()` method. */ -export interface DesktopHubEventBridgeOptions { +interface DesktopHubEventBridgeOptions { /** hubClient for incremental message resync on reconnect/gap (#2101 G4-②). */ hubClient?: MessagesResyncHubClient; } diff --git a/app/desktop/src/stores/modelSettingsStore.ts b/app/desktop/src/stores/modelSettingsStore.ts index 78ae07fac..e2e33ff77 100644 --- a/app/desktop/src/stores/modelSettingsStore.ts +++ b/app/desktop/src/stores/modelSettingsStore.ts @@ -1,11 +1,11 @@ import { create } from 'zustand'; import { persist, subscribeWithSelector } from 'zustand/middleware'; -export type ReasoningEffortPreference = 'low' | 'medium' | 'high' | 'max'; -export type ProviderHealth = 'ready' | 'degraded' | 'disabled'; -export type CredentialTestResult = 'idle' | 'connecting' | 'success' | 'error'; +type ReasoningEffortPreference = 'low' | 'medium' | 'high' | 'max'; +type ProviderHealth = 'ready' | 'degraded' | 'disabled'; +type CredentialTestResult = 'idle' | 'connecting' | 'success' | 'error'; -export interface ModelAliasMapping { +interface ModelAliasMapping { alias: string; model: string; provider: string; @@ -13,7 +13,7 @@ export interface ModelAliasMapping { enabled: boolean; } -export interface CcSwitchProvider { +interface CcSwitchProvider { id: string; name: string; health: ProviderHealth; @@ -21,7 +21,7 @@ export interface CcSwitchProvider { notes: string; } -export interface ProviderCredential { +interface ProviderCredential { providerId: string; apiKey: string; enabled: boolean; @@ -30,7 +30,6 @@ export interface ProviderCredential { testError: string; } -const CREDENTIAL_SALT = 'ah-creds-v1'; interface ModelSettingsState { defaultModel: string; @@ -57,14 +56,14 @@ interface ModelSettingsState { reset: () => void; } -export interface RunModelSettingsInput { +interface RunModelSettingsInput { model?: string; provider?: string; modelAlias?: string; reasoningEffort?: string; } -export interface ResolvedRunModelSettings { +interface ResolvedRunModelSettings { model?: string; provider?: string; reasoningEffort?: string; @@ -164,34 +163,8 @@ const cloneAliases = () => DEFAULT_ALIASES.map((item) => ({ ...item })); const cloneCcSwitchProviders = () => DEFAULT_CC_SWITCH_PROVIDERS.map((item) => ({ ...item })); const cloneCredentials = () => DEFAULT_CREDENTIALS.map((item) => ({ ...item })); -function obscureApiKey(raw: string): string { - if (!raw) return ''; - try { - const salted = CREDENTIAL_SALT + raw; - return btoa(salted); - } catch { - return ''; - } -} -function revealApiKey(obscured: string): string { - if (!obscured) return ''; - try { - const decoded = atob(obscured); - if (decoded.startsWith(CREDENTIAL_SALT)) { - return decoded.slice(CREDENTIAL_SALT.length); - } - return decoded; - } catch { - return obscured; - } -} -function maskApiKey(raw: string): string { - if (!raw) return ''; - if (raw.length <= 8) return '*'.repeat(raw.length); - return raw.slice(0, 4) + '*'.repeat(Math.max(raw.length - 8, 4)) + raw.slice(-4); -} function migrateProviderId(provider: string | undefined): string | undefined { return provider === LEGACY_TOKENDANCE_RELAY_PROVIDER_ID ? TOKENDANCE_GATEWAY_PROVIDER_ID : provider; @@ -321,8 +294,3 @@ export const useModelSettingsStore = create()( ), ), ); - -export const DEFAULT_MODEL_ALIASES = DEFAULT_ALIASES; -export const DEFAULT_CC_SWITCH_PROVIDER_STATUS = DEFAULT_CC_SWITCH_PROVIDERS; - -export { obscureApiKey, revealApiKey, maskApiKey }; diff --git a/app/desktop/src/stores/notificationStore.ts b/app/desktop/src/stores/notificationStore.ts index a4b62b8b2..8474ffe01 100644 --- a/app/desktop/src/stores/notificationStore.ts +++ b/app/desktop/src/stores/notificationStore.ts @@ -1,7 +1,7 @@ import { create } from 'zustand'; import { subscribeWithSelector } from 'zustand/middleware'; -export type NotificationType = 'friend_request' | 'agent_task' | 'message' | 'system'; +type NotificationType = 'friend_request' | 'agent_task' | 'message' | 'system'; export interface Notification { id: string; diff --git a/app/mobile-rn/src/components/primitives/BottomSheet.motion.ts b/app/mobile-rn/src/components/primitives/BottomSheet.motion.ts index 28914cd73..4fd6d951f 100644 --- a/app/mobile-rn/src/components/primitives/BottomSheet.motion.ts +++ b/app/mobile-rn/src/components/primitives/BottomSheet.motion.ts @@ -5,10 +5,6 @@ const TABLET_SHEET_MAX_WIDTH = 640; const TABLET_SIDE_MARGIN = 48; const SHEET_MAX_HEIGHT_RATIO = 0.9; -export const BOTTOM_SHEET_DRAG_DISMISS_DISTANCE = 72; -export const BOTTOM_SHEET_DRAG_DISMISS_VELOCITY = 0.9; -export const BOTTOM_SHEET_DRAG_EXIT_OFFSET = 96; - export interface BottomSheetFrame { maxHeight: number; maxWidth?: number; diff --git a/app/mobile-rn/src/platform/mobilePlatform.ts b/app/mobile-rn/src/platform/mobilePlatform.ts index fefeba556..ef17ea73b 100644 --- a/app/mobile-rn/src/platform/mobilePlatform.ts +++ b/app/mobile-rn/src/platform/mobilePlatform.ts @@ -26,7 +26,8 @@ const mobileCapabilities: SurfaceCapabilities = { // New capability domains intentionally un-declared on Mobile: Hub client // currently exposes no approval/runtimeEvidence/sandbox contract, and // remote execution is not wired. UI must hide related affordances until - // a Mobile-specific Hub channel lands. See BLOCKED.md for revisit trigger. + // a Mobile-specific Hub channel lands. Revisit when the Mobile Hub client + // exposes approval/runtimeEvidence/sandbox contracts or remote execution. }; function mapFixtureToConversations(fixture: MobileAppFixture): WorkbenchConversation[] { diff --git a/app/mobile-rn/src/theme/motion.ts b/app/mobile-rn/src/theme/motion.ts index b3d567f4a..e69b2b186 100644 --- a/app/mobile-rn/src/theme/motion.ts +++ b/app/mobile-rn/src/theme/motion.ts @@ -172,14 +172,3 @@ export const motion = { export function shouldReduceMotion(accessibilityReduceMotion: boolean | null | undefined): boolean { return accessibilityReduceMotion === true; } - -export function resolveMotionTiming( - timing: MotionTimingSpec, - accessibilityReduceMotion: boolean | null | undefined, -): MotionTimingSpec { - if (shouldReduceMotion(accessibilityReduceMotion)) { - return { durationMs: motion.reduced.durationMs, easing: 'standard' }; - } - - return timing; -} diff --git a/app/shared/src/diff.ts b/app/shared/src/diff.ts index 257b12696..7aae16ee7 100644 --- a/app/shared/src/diff.ts +++ b/app/shared/src/diff.ts @@ -286,9 +286,12 @@ export function parseUnifiedDiff( } } -// ── Go-ported extraction / validation ─────────── -// These match edge-server/internal/diff/diff.go semantics. -// Field names (file/patch) mirror the wire format from OpenCode diffs.ts. +// ── TS-side extraction / validation ───────────── +// No Go counterpart: edge-server/internal/diff was removed in #2151 +// (zero references). Edge-server only generates unified diffs +// (internal/adapters/surfacing_diff.go) and applies hunk decisions +// (internal/api/diff_apply.go). Field names (file/patch) mirror the wire +// format from OpenCode diffs.ts. export interface DiffInput { file: string; diff --git a/app/shared/src/hub/hubClientTeamTypes.ts b/app/shared/src/hub/hubClientTeamTypes.ts index ac22c7a5e..2a3c97d37 100644 --- a/app/shared/src/hub/hubClientTeamTypes.ts +++ b/app/shared/src/hub/hubClientTeamTypes.ts @@ -430,15 +430,11 @@ export type AgentTeamTask = HubAgentTeamTask; export type AgentTeamEvent = HubAgentTeamEvent; export type TeamMemberState = HubTeamMemberState; export type TeamTaskState = HubTeamTaskState; -export type TeamTaskDependencyState = HubTeamTaskDependencyState; export type TeamAssignmentState = HubTeamAssignmentState; export type TeamApprovalState = HubTeamApprovalState; export type TeamArtifactState = HubTeamArtifactState; export type TeamConflictState = HubTeamConflictState; export type TeamRunEventState = HubTeamRunEventState; -export type TeamRouteAuditState = HubTeamRouteAuditState; -export type HumanReviewChange = HubHumanReviewChange; -export type HumanReviewState = HubHumanReviewState; export type TeamBudget = HubTeamBudget; export type TeamRunState = HubTeamRunState; export type TeamApprovalDecisionRequest = HubTeamApprovalDecisionRequest; diff --git a/app/shared/src/hub/hubWS.ts b/app/shared/src/hub/hubWS.ts index e8efd7592..7d0402c60 100644 --- a/app/shared/src/hub/hubWS.ts +++ b/app/shared/src/hub/hubWS.ts @@ -45,7 +45,7 @@ export interface HubWSOptions { useQueryTokenFallback?: boolean; } -/** Payload emitted on HUB_WS_GAP_EVENT when a seq_id discontinuity is observed. */ +/** Payload handed to HubWSHandle.onGap when a seq_id discontinuity is observed. */ export interface HubWSGapPayload { /** Last successfully processed seq_id on this connection. */ lastSeq: number; @@ -55,22 +55,6 @@ export interface HubWSGapPayload { gapSize: number; } -/** - * Internal-only event name for seq_id gap detection. Not part of HUB_EVENTS - * because it has no server-side producer; it is synthesized client-side by - * hubWS when per-connection seq_id is discontinuous. Subscribe via - * HubWSHandle.onGap(). See #2101 G1. - */ -export const HUB_WS_GAP_EVENT = 'hub.ws.gap'; - -/** - * Internal-only event name for post-reconnect auth completion. Synthesized - * client-side when auth.ok arrives on a connection that has previously been - * authenticated (i.e. a reconnect, not the first connect). Subscribe via - * HubWSHandle.onReconnected(). See #2101 G4-②. - */ -export const HUB_WS_RECONNECTED_EVENT = 'hub.ws.reconnected'; - export interface HubWSHandle { /** Open the WebSocket connection and initiate auth handshake. */ connect: () => void; diff --git a/app/shared/src/index.ts b/app/shared/src/index.ts index 80034ef70..9caccc1f6 100644 --- a/app/shared/src/index.ts +++ b/app/shared/src/index.ts @@ -114,7 +114,7 @@ export type { ReviewDiff, } from './diff'; -// Go-ported extraction / validation (edge-server/internal/diff/diff.go) +// TS-side extraction / validation (no Go counterpart — edge-server/internal/diff removed in #2151) export { isDiff, extractDiffs, isObj } from './diff'; export type { DiffInput } from './diff'; diff --git a/app/shared/src/inspector/inspectorEvidence.test.ts b/app/shared/src/inspector/inspectorEvidence.test.ts index 461f456f1..984ce7b30 100644 --- a/app/shared/src/inspector/inspectorEvidence.test.ts +++ b/app/shared/src/inspector/inspectorEvidence.test.ts @@ -20,7 +20,7 @@ describe('buildInspectorEvidenceModel', () => { const evidence: EvidenceRef[] = [ { id: 'run-1', kind: 'run', label: 'Run 1', status: 'running' }, { id: 'tool-rg', kind: 'tool', label: 'rg desktop', status: 'completed' }, - { id: 'file-app', kind: 'file', label: 'app/shared/src/workbench/RightInspector.tsx' }, + { id: 'file-app', kind: 'file', label: 'app/workbench/src/RightInspector.tsx' }, { id: 'artifact-smoke', kind: 'artifact', label: 'visual-smoke-desktop.png', status: 'completed' }, ]; diff --git a/app/shared/src/surfaceMetadata.ts b/app/shared/src/surfaceMetadata.ts index bbf4bf5ee..1306782af 100644 --- a/app/shared/src/surfaceMetadata.ts +++ b/app/shared/src/surfaceMetadata.ts @@ -328,15 +328,6 @@ export function getSurfaceByDesktopSectionId(sectionId: string): SurfaceMetadata ); } -export function getSurfaceByWebRoute(route: string): SurfaceMetadata | undefined { - return (SURFACE_METADATA as readonly SurfaceMetadata[]).find( - (surface) => - surface.platform === 'web' && - typeof surface.webRoutePattern === 'string' && - matchesRoutePattern(route, surface.webRoutePattern), - ); -} - function matchesRoutePattern(route: string, pattern: string): boolean { const routeParts = trimSlashes(route).split('/').filter(Boolean); const patternParts = trimSlashes(pattern).split('/').filter(Boolean); diff --git a/app/shared/src/testing/e2eDataModeContract.ts b/app/shared/src/testing/e2eDataModeContract.ts index fdba6db77..2f4104ed0 100644 --- a/app/shared/src/testing/e2eDataModeContract.ts +++ b/app/shared/src/testing/e2eDataModeContract.ts @@ -163,10 +163,6 @@ export function assertE2EDataModeScenario( } } -export function isE2ERequestAllowed(scenario: E2EDataModeScenario, request: E2EObservedRequest): boolean { - return isBoundaryAllowed(scenario, classifyE2ERequest(request.url, scenario), request); -} - export function buildE2EDataModeManifest( scenario: E2EDataModeScenario, requests: E2EObservedRequest[] = [], diff --git a/app/shared/src/transcript/normalizeEdgeEvents.test.ts b/app/shared/src/transcript/normalizeEdgeEvents.test.ts index 7a4df8e4a..91d5fba5a 100644 --- a/app/shared/src/transcript/normalizeEdgeEvents.test.ts +++ b/app/shared/src/transcript/normalizeEdgeEvents.test.ts @@ -772,7 +772,8 @@ describe('normalizeEdgeEventsToTranscript edge cases', () => { // and were previously dropped by the default console.warn branch. it('wires run.agent.mcp_tool_call to a tool_call block (MCP server tool activity)', () => { - // Emitted by edge-server/internal/adapters/codex_emit_tools.go alongside + // Emitted by edge-server/internal/adapters/parser_ndjson_parse_msg.go:29,68 + // (event name const: orchestration/contracts.go:163) alongside // run.agent.tool_call; same payload shape (toolName, callId, input). const blocks = normalizeEdgeEventsToTranscript([ edgeEvent('evt-mcp-tool', 1, 'run.agent.mcp_tool_call', { diff --git a/app/shared/src/transcript/normalizeHubMessages.ts b/app/shared/src/transcript/normalizeHubMessages.ts index 6b2ed5740..f89c56897 100644 --- a/app/shared/src/transcript/normalizeHubMessages.ts +++ b/app/shared/src/transcript/normalizeHubMessages.ts @@ -77,32 +77,23 @@ const ATTACHMENT_MISSING_FILE_FALLBACK = '文件附件缺失'; export type NormalizeHubTranslate = (key: string) => string; /** - * pin 状态来源(已落地,2026-08-02 专项清理时确认闭环): + * pin 状态来源(已落地): * - * Survey (2026-08-01, sonnet-unpin-recall 续23 → unpin menu entry): - * - hub-server `model.Message` has no `pinned` field; pins live in the - * separate `message_pins` table (`model.MessagePin`), surfaced only via - * REST `GET /client/sessions/{id}/pins` — which the frontend has no runtime - * consumer for (only e2e mocks / payload path builders). - * - WS frames `message.pin` (payload: session_id, message_id, - * pinned_by_user_id, pinned_at) and `message.unpin` (session_id, - * message_id) carry the pin events, but the consumers only refresh: - * web (webHubRealtime.ts) invalidates the hub-messages query — whose - * re-fetched payload still has no pin field — and desktop - * (useHubEventStream.ts / hubEventBridge.ts) just touches `lastMessage`. - * - `hubClientDomainTypes.HubMessage.pinned` was deliberately NOT added: - * the REST message shape has no such field, so it would be a dead field. - * - * Landed store path: the web / desktop WS handlers maintain a - * session-scoped `messageId → pinned` map (pinMap.ts, fed by the - * MESSAGE_PIN/MESSAGE_UNPIN frames, seeded from `GET /client/sessions/{id}/pins`), - * and the normalize callers (webWorkbenchTranscript.ts / - * useDesktopWorkbenchModel.ts) merge the map via `withPinnedState` into - * `HubMessageTranscriptInput.pinned` before calling this function — the - * adapter below writes it through to `block.pinned`, and the context menu - * toggles pin/unpin off `block.pinned`. + * - hub-server `model.Message` 没有 `pinned` 字段;pin 存在独立的 + * `message_pins` 表(`model.MessagePin`),经 REST + * `GET /client/sessions/{id}/pins` 上浮。 + * - REST 消费方两端都有:desktop `sessionQueries.useHubPinnedMessages` + * (queryKey `['hub','threads',sessionId,'pins']`);web + * `useWebWorkbenchModel`(queryKey `['web-v4','hub-pins',...]`), + * 两者都用来给 pinMap store 做种子。 + * - WS 帧 `message.pin` / `message.unpin` 直接驱动 pinMap store:desktop + * `hubEventBridge.ts` 失效 `threads.pins`/`threads.detail` 并调用 + * `getPinMapStore().handleFrame`;web `webHubRealtime.ts` 同样把帧喂给 + * pinMap,`useWebWorkbenchModel` 订阅 pinMap 并以 REST pins 结果种子化。 + * - `HubMessage.pinned` 仍没有加:REST message 形状确实没有该字段,pin + * 状态统一放在 pinMap(`messageId → pinned`),由 normalize 调用方经 + * `withPinnedState` 合并进输入,本文件写入 `block.pinned`。 */ - export function normalizeHubMessagesToTranscript( messages: HubMessageTranscriptInput[] | undefined, t?: NormalizeHubTranslate, diff --git a/app/shared/src/transcript/normalizeThreadItems.test.ts b/app/shared/src/transcript/normalizeThreadItems.test.ts index f04a043f0..6f8cad5e4 100644 --- a/app/shared/src/transcript/normalizeThreadItems.test.ts +++ b/app/shared/src/transcript/normalizeThreadItems.test.ts @@ -65,7 +65,7 @@ describe('normalizeThreadItemsToTranscript', () => { itemId: 'diff-1', type: 'diff', role: 'agent', - content: 'app/desktop/src/App.tsx\napp/shared/src/workbench/AgentHubWorkbench.tsx', + content: 'app/desktop/src/App.tsx\napp/workbench/src/AgentHubWorkbench.tsx', runId: 'run-2', createdAt: '2026-06-07T02:00:00Z', }, @@ -83,7 +83,7 @@ describe('normalizeThreadItemsToTranscript', () => { expect(blocks[0]).toEqual(expect.objectContaining({ kind: 'diff', title: 'app/desktop/src/App.tsx', - files: ['app/desktop/src/App.tsx', 'app/shared/src/workbench/AgentHubWorkbench.tsx'], + files: ['app/desktop/src/App.tsx', 'app/workbench/src/AgentHubWorkbench.tsx'], })); expect(blocks[1]).toEqual(expect.objectContaining({ kind: 'approval', diff --git a/app/shared/src/transcript/transcriptEvidence.test.ts b/app/shared/src/transcript/transcriptEvidence.test.ts index 58edae5bf..0503699ef 100644 --- a/app/shared/src/transcript/transcriptEvidence.test.ts +++ b/app/shared/src/transcript/transcriptEvidence.test.ts @@ -25,7 +25,7 @@ describe('collectTranscriptEvidence', () => { status: 'completed', evidenceRefs: [ { id: 'ev-tool', kind: 'tool', label: 'rg desktop shell', status: 'completed' }, - { id: 'ev-file', kind: 'file', label: 'app/shared/src/workbench/AgentHubWorkbench.tsx' }, + { id: 'ev-file', kind: 'file', label: 'app/workbench/src/AgentHubWorkbench.tsx' }, ], }, { @@ -51,7 +51,7 @@ describe('collectTranscriptEvidence', () => { kind: 'diff', author: { id: 'builder', name: 'Builder', role: 'agent' }, title: 'Shared shell diff', - files: ['app/shared/src/workbench/AgentHubWorkbench.tsx'], + files: ['app/workbench/src/AgentHubWorkbench.tsx'], evidenceRefs: [{ id: 'ev-file', kind: 'file', label: 'Workbench file' }], }, { diff --git a/app/shared/src/ui/DiffReviewPanelHelpers.ts b/app/shared/src/ui/DiffReviewPanelHelpers.ts index 8847a8f10..e71f9c87a 100644 --- a/app/shared/src/ui/DiffReviewPanelHelpers.ts +++ b/app/shared/src/ui/DiffReviewPanelHelpers.ts @@ -10,13 +10,13 @@ import styles from './DiffReviewPanel.module.css'; // ── Build side-by-side rows from a hunk ──────────────────────────────── -export function makeCell(lineNumber: number | undefined, content: string): SideBySideCell { +function makeCell(lineNumber: number | undefined, content: string): SideBySideCell { const cell: SideBySideCell = { content }; if (lineNumber != null) cell.lineNumber = lineNumber; return cell; } -export function makeRow( +function makeRow( left: SideBySideCell | null, right: SideBySideCell | null, rowType: SideBySideRow['rowType'], diff --git a/app/shared/vitest.config.ts b/app/shared/vitest.config.ts index fd123d152..155dd7a8b 100644 --- a/app/shared/vitest.config.ts +++ b/app/shared/vitest.config.ts @@ -42,13 +42,11 @@ export default defineConfig({ // index.ts — 纯 re-export 入口 // types.ts — 纯类型声明 // events.ts — 事件名常量(无逻辑) - // mock.ts — 测试 mock 工具(不参与被测逻辑) // errors.ts — 错误类型定义 exclude: [ 'src/index.ts', 'src/types.ts', 'src/events.ts', - 'src/mock.ts', 'src/errors.ts', ], }), diff --git a/app/web/src/api/agentQueries.ts b/app/web/src/api/agentQueries.ts index 58d2b3a22..0019e02c1 100644 --- a/app/web/src/api/agentQueries.ts +++ b/app/web/src/api/agentQueries.ts @@ -295,33 +295,6 @@ export function agentConfigToUpdateAgentProfileRequest(agent: AgentConfig, t?: ( }; } -export function createDefaultAgentProfileRequest( - index: number, - // The only web mapper callback that actually interpolates — it calls - // t('agents.newDefault', { index }) — so it is the only one that keeps an - // options bag, and it is typed rather than `any` (which is what - // `eslint --max-warnings 0` in the web lint gate rejects). - // - // The other five web copies of this signature declared the same parameter and - // never passed anything, so they dropped it. Do not "re-symmetrise" this with - // desktop: desktop's six copies keep `options?: any` on purpose, because their - // callers hand in i18next's own `TFunction`, which is only assignable to a - // callback whose options parameter is `any` — narrowing it there fails - // typecheck with TS2345 at App.tsx:265 (measured, not guessed). desktop's - // eslint config does not enable no-explicit-any, so nothing flags it. - t?: (key: string, options?: Record) => string, -): CreateAgentProfileRequest { - return { - name: t?.('agents.newDefault', { index }) ?? `新 Agent ${index}`, - runtime_id: 'codex', - model: 'gpt-5-codex', - provider: 'codex', - reasoning_effort: 'medium', - permission_mode: 'default', - skills: '[]', - tool_allowlist: '[]', - }; -} async function fetchHubAgentProfiles(token: string): Promise> { const client = createHubClient({ getToken: () => token }); diff --git a/app/web/src/api/contactQueries.ts b/app/web/src/api/contactQueries.ts index 2ed75736e..88fe906d1 100644 --- a/app/web/src/api/contactQueries.ts +++ b/app/web/src/api/contactQueries.ts @@ -4,8 +4,8 @@ import { getAccessToken } from '@/hooks/useAuth'; import { hubQueryKeys } from '@shared/stores/queryKeys'; import type { FriendRequestInfo, SearchResult } from './hubClient'; -export const contactsQueryKey = hubQueryKeys.contacts.list; -export const friendRequestsQueryKey = hubQueryKeys.contacts.friendRequests; +const contactsQueryKey = hubQueryKeys.contacts.list; +const friendRequestsQueryKey = hubQueryKeys.contacts.friendRequests; export const sessionsQueryKey = hubQueryKeys.threads.root; // ── Async helpers ────────────────────────────────────────── diff --git a/app/web/src/api/executionTargetQueries.test.ts b/app/web/src/api/executionTargetQueries.test.ts index 8e40a67f2..51ecd8026 100644 --- a/app/web/src/api/executionTargetQueries.test.ts +++ b/app/web/src/api/executionTargetQueries.test.ts @@ -59,6 +59,85 @@ describe('web execution target queries', () => { }); }); + it('walks cursor pages up to the shared 50x10 cap and reports hasMore honestly', async () => { + vi.mocked(getAccessToken).mockReturnValue('hub-access'); + const fetchMock = vi.fn() + .mockResolvedValueOnce(new Response( + JSON.stringify({ + code: 'ok', + data: { + items: [{ + id: '00000000-0000-0000-0000-00000000e101', + name: 'Page 1 Target', + target_type: 'local_edge', + workspace_allowlist: [], + trust_level: 'local', + health_state: 'online', + is_online: true, + }], + page: { hasMore: true, nextCursor: 'cur-1' }, + }, + }), + { status: 200, headers: { 'Content-Type': 'application/json' } }, + )) + .mockResolvedValueOnce(new Response( + JSON.stringify({ + code: 'ok', + data: { + items: [{ + id: '00000000-0000-0000-0000-00000000e102', + name: 'Page 2 Target', + target_type: 'remote_ssh', + workspace_allowlist: [], + trust_level: 'remote', + health_state: 'healthy', + is_online: false, + }], + page: { hasMore: false }, + }, + }), + { status: 200, headers: { 'Content-Type': 'application/json' } }, + )); + vi.stubGlobal('fetch', fetchMock); + + const res = await fetchExecutionTargets(true); + + expect(fetchMock).toHaveBeenCalledTimes(2); + expect(String(fetchMock.mock.calls[0]?.[0])).toBe('http://localhost:8080/web/execution-targets?pageSize=50'); + expect(String(fetchMock.mock.calls[1]?.[0])).toBe('http://localhost:8080/web/execution-targets?pageSize=50&pageCursor=cur-1'); + expect(res.items).toHaveLength(2); + expect(res.page.hasMore).toBe(false); + }); + + it('reports hasMore=true after exhausting the maximum cursor pages', async () => { + vi.mocked(getAccessToken).mockReturnValue('hub-access'); + const fetchMock = vi.fn(async () => new Response( + JSON.stringify({ + code: 'ok', + data: { + items: [{ + id: '00000000-0000-0000-0000-00000000e201', + name: 'Always More', + target_type: 'local_edge', + workspace_allowlist: [], + trust_level: 'local', + health_state: 'registered', + is_online: false, + }], + page: { hasMore: true, nextCursor: 'cur-next' }, + }, + }), + { status: 200, headers: { 'Content-Type': 'application/json' } }, + )); + vi.stubGlobal('fetch', fetchMock); + + const res = await fetchExecutionTargets(true); + + expect(fetchMock).toHaveBeenCalledTimes(10); + expect(res.items).toHaveLength(10); + expect(res.page.hasMore).toBe(true); + }); + it('does not fall back to static target previews when Hub auth is missing', async () => { const fetchMock = vi.fn(); vi.stubGlobal('fetch', fetchMock); @@ -288,6 +367,39 @@ describe('web execution target queries', () => { ]); }); + it('keeps registered health states in the pass-through instead of collapsing to unknown', async () => { + vi.mocked(getAccessToken).mockReturnValue('hub-access'); + vi.stubGlobal( + 'fetch', + vi.fn( + async () => + new Response( + JSON.stringify({ + code: 'ok', + data: { + items: [ + { + id: 'registered-1', + name: 'Bound but not proven live', + target_type: 'local_edge', + workspace_allowlist: [], + trust_level: 'local', + health_state: 'registered', + is_online: false, + }, + ], + page: { hasMore: false }, + }, + }), + { status: 200, headers: { 'Content-Type': 'application/json' } } + ) + ) + ); + + const result = await fetchExecutionTargets(true); + expect(result.items[0]?.health_state).toBe('registered'); + }); + it('counts unknown health states and preserves zero-filled type buckets', () => { expect( summarizeExecutionTargets([ diff --git a/app/web/src/api/executionTargetQueries.ts b/app/web/src/api/executionTargetQueries.ts index 4700b981b..3939ad268 100644 --- a/app/web/src/api/executionTargetQueries.ts +++ b/app/web/src/api/executionTargetQueries.ts @@ -53,18 +53,21 @@ const executionTargetTypes: ExecutionTargetType[] = [ const trustLevels: ExecutionTargetTrustLevel[] = ['local', 'remote', 'cloud', 'relay']; const healthStates: ExecutionTargetHealthState[] = [ 'unknown', - 'healthy', 'online', + 'healthy', 'degraded', 'offline', - 'mismatch', 'stale', + 'mismatch', + 'registered', ]; const emptyExecutionTargets: ExecutionTargetInventoryResponse = { items: [], page: { hasMore: false }, }; +const executionTargetPageSize = 50; +const maxExecutionTargetPages = 10; function parseWorkspaceAllowlist(value: ExecutionTarget['workspace_allowlist']): string[] { if (Array.isArray(value)) { @@ -112,10 +115,26 @@ export async function fetchExecutionTargets( if (!preferHub || !token) return emptyExecutionTargets; const client = createHubClient({ getToken: () => token }); - const res = await client.listExecutionTargets({ pageSize: 50 }); + const items: ExecutionTargetInventoryItem[] = []; + let page: ExecutionTargetListResponse['page'] = { hasMore: false }; + let pageCursor: string | undefined; + + for (let i = 0; i < maxExecutionTargetPages; i += 1) { + const res = await client.listExecutionTargets({ + pageSize: executionTargetPageSize, + ...(pageCursor ? { pageCursor } : {}), + }); + items.push(...res.items.map(normalizeExecutionTarget)); + page = res.page ?? { hasMore: false }; + if (!page.hasMore || !page.nextCursor) { + return { items, page }; + } + pageCursor = page.nextCursor; + } + return { - items: res.items.map(normalizeExecutionTarget), - page: res.page, + items, + page: { ...page, hasMore: true }, }; } diff --git a/app/web/src/api/hubClient.ts b/app/web/src/api/hubClient.ts index 82a958580..be52e4b5a 100644 --- a/app/web/src/api/hubClient.ts +++ b/app/web/src/api/hubClient.ts @@ -43,32 +43,5 @@ export function createHubClient(opts: HubClientOptions = {}): SharedHubClient { // Compatibility shims used by existing web inventory UI (historical shapes). -export type ExecutionTargetHealthState = 'unknown' | 'healthy' | 'degraded' | 'offline' | string; -export type ExecutionTargetTrustLevel = 'local' | 'remote' | 'cloud' | 'relay' | string; - -export interface AgentInstance { - id: string; - agent_type: string; - custom_agent_id?: string; - session_id: string; - inviter_user_id: string; - workspace_id?: string; - display_name: string; - created_at?: string; -} - -export interface PendingAgentTask { - id: string; - agent_instance_id: string; - triggered_by_user_id: string; - trigger_message_id: string; - target_id?: string; - status: string; - edge_run_id?: string; - edge_device_id?: string; - error_message?: string; - created_at?: string; - dispatched_at?: string; - finished_at?: string; - expire_at?: string; -} +export type ExecutionTargetHealthState = 'unknown' | 'online' | 'healthy' | 'degraded' | 'offline' | 'stale' | 'mismatch' | 'registered'; +export type ExecutionTargetTrustLevel = 'local' | 'remote' | 'cloud' | 'relay'; diff --git a/app/web/src/api/projectQueries.ts b/app/web/src/api/projectQueries.ts index 19278833b..b5d46831a 100644 --- a/app/web/src/api/projectQueries.ts +++ b/app/web/src/api/projectQueries.ts @@ -13,7 +13,7 @@ import type { WorkspaceProjectThreadMessage, } from './hubClient'; -export const workspaceProjectsQueryKey = hubQueryKeys.projects.root; +const workspaceProjectsQueryKey = hubQueryKeys.projects.root; // Deliberately no local threads-key alias next to the projects key above: the // one that used to sit here had 0 consumers and pointed at // `hubQueryKeys.projects.root` — the *projects* root — so any invalidation @@ -205,34 +205,3 @@ export function useHubWorkspaceProjectThreadMessages(options: { placeholderData: (previous) => previous, }); } - -export function useCreateHubWorkspaceProjectThread(options: { getToken?: () => string | null } = {}) { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ({ projectId, draft }: { projectId: string; draft: CreateWorkspaceProjectThreadRequest }) => - createWorkspaceProjectThread(projectId, draft, options.getToken ?? getAccessToken), - onSettled: (_data, _error, variables) => { - void queryClient.invalidateQueries({ queryKey: hubQueryKeys.projects.threads(variables.projectId) }); - }, - }); -} - -export function useSendHubWorkspaceProjectThreadMessage(options: { getToken?: () => string | null } = {}) { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ( - { projectId, threadId, draft }: { - projectId: string; - threadId: string; - draft: SendWorkspaceProjectThreadMessageRequest; - }, - ) => sendWorkspaceProjectThreadMessage(projectId, threadId, draft, options.getToken ?? getAccessToken), - onSettled: (_data, _error, variables) => { - void queryClient.invalidateQueries({ - queryKey: hubQueryKeys.projects.threadMessages(variables.projectId, variables.threadId), - }); - }, - }); -} diff --git a/app/web/src/api/runEventReplay.ts b/app/web/src/api/runEventReplay.ts index 2e2b2eec2..fdcc70f40 100644 --- a/app/web/src/api/runEventReplay.ts +++ b/app/web/src/api/runEventReplay.ts @@ -8,9 +8,9 @@ import type { HubClient } from '@/api/hubClient'; import type { HubRuntimeEventTranscriptInput } from '@shared/transcript'; -import { useConnectionStore, type RecoveryState } from '@/stores/connectionStore'; +import { useConnectionStore } from '@/stores/connectionStore'; -export interface ReplayControllerOptions { +interface ReplayControllerOptions { hubClient: HubClient; /** Currently active agent task ID (may change over time). */ getActiveTaskId: () => string | undefined; @@ -89,29 +89,7 @@ export async function replayMissedEvents(opts: ReplayControllerOptions): Promise * Create a recovery state transcript block that can be inserted * into the transcript to indicate where replay events were inserted. */ -export function replayGapBlock( - taskId: string, - replayedCount: number, - recovering: boolean = false, -): HubRuntimeEventTranscriptInput { - return { - id: `replay-gap-${taskId}`, - task_id: taskId, - event_type: 'replay.gap', - payload: { - kind: 'replay_gap', - replayedCount, - taskId, - recovering, - }, - created_at: new Date().toISOString(), - }; -} /** * Read the current recovery state from the connection store. */ -export function getRecoveryState(): { state: RecoveryState; error: string | null } { - const store = useConnectionStore.getState(); - return { state: store.recoveryState, error: store.recoveryError }; -} diff --git a/app/web/src/components/StartConversationModal.tsx b/app/web/src/components/StartConversationModal.tsx index d92c2a4a9..37aa60287 100644 --- a/app/web/src/components/StartConversationModal.tsx +++ b/app/web/src/components/StartConversationModal.tsx @@ -18,7 +18,7 @@ import styles from './StartConversationModal.module.css'; presentational (filter + list + busy/error display). ═══════════════════════════════════════════════════════════════════════ */ -export interface StartConversationModalProps { +interface StartConversationModalProps { open: boolean; /** Real contact list for the current user (already resolved via Hub). */ members: ContactMember[]; diff --git a/app/web/src/config.ts b/app/web/src/config.ts index 1f79b8b1f..ad5822c12 100644 --- a/app/web/src/config.ts +++ b/app/web/src/config.ts @@ -12,5 +12,4 @@ export const HUB_URL = envOrDev('VITE_HUB_URL', 'http://localhost:8080'); export const HUB_WS_URL = envOrDev('VITE_HUB_WS_URL', 'ws://localhost:8080/client/ws'); export const HEALTH_POLL_MS = 5000; export const RUNNERS_POLL_MS = 10000; -export const EVENT_LOG_MAX = 1000; export const APP_VERSION = '0.4.1'; diff --git a/app/web/src/i18n/index.ts b/app/web/src/i18n/index.ts index ba8025a76..5c9e29f90 100644 --- a/app/web/src/i18n/index.ts +++ b/app/web/src/i18n/index.ts @@ -7,11 +7,11 @@ import zhCommon from './locales/zh/common.json'; import enCommon from './locales/en/common.json'; -export type AppLanguage = 'en' | 'zh'; +type AppLanguage = 'en' | 'zh'; const LANGUAGE_STORAGE_KEY = 'agenthub-language'; -export function normalizeLanguage(value: string | null | undefined): AppLanguage { +function normalizeLanguage(value: string | null | undefined): AppLanguage { return value?.toLowerCase().startsWith('zh') ? 'zh' : 'en'; } @@ -34,19 +34,10 @@ function detectBrowserLanguage(): AppLanguage { return normalizeLanguage(language); } -export function getInitialLanguage(): AppLanguage { +function getInitialLanguage(): AppLanguage { return readStoredLanguage() ?? detectBrowserLanguage() ?? 'en'; } -export function setLanguagePreference(language: AppLanguage): void { - try { - localStorage.setItem(LANGUAGE_STORAGE_KEY, language); - } catch { - // localStorage may be unavailable in private mode or embedded contexts. - } - - void i18n.changeLanguage(language); -} i18n.use(initReactI18next).init({ resources: { diff --git a/app/web/src/i18n/locales/en/common.json b/app/web/src/i18n/locales/en/common.json index e1572b915..33c2f12b5 100644 --- a/app/web/src/i18n/locales/en/common.json +++ b/app/web/src/i18n/locales/en/common.json @@ -95,7 +95,6 @@ "error.code.agent_offline": "The agent is currently offline. Please wait for it to reconnect or choose another target.", "error.code.target_not_routable": "The selected execution target is unavailable. Please choose a different target.", "agents.unnamed": "Unnamed agent", - "agents.newDefault": "New agent {{index}}", "conversations.hubPrivate": "Hub direct", "conversations.hubGroup": "Hub group", "projects.unnamed": "Unnamed project", diff --git a/app/web/src/i18n/locales/zh/common.json b/app/web/src/i18n/locales/zh/common.json index 28288a40d..6eaac50a3 100644 --- a/app/web/src/i18n/locales/zh/common.json +++ b/app/web/src/i18n/locales/zh/common.json @@ -95,7 +95,6 @@ "error.code.agent_offline": "Agent 当前离线,请等待重连或选择其他执行目标。", "error.code.target_not_routable": "所选执行目标不可用,请选择其他目标。", "agents.unnamed": "未命名 Agent", - "agents.newDefault": "新 Agent {{index}}", "conversations.hubPrivate": "Hub 私聊", "conversations.hubGroup": "Hub 群聊", "projects.unnamed": "未命名项目", diff --git a/app/web/src/platform/useWebWorkbenchModel.ts b/app/web/src/platform/useWebWorkbenchModel.ts index a246c0206..8e0d9598e 100644 --- a/app/web/src/platform/useWebWorkbenchModel.ts +++ b/app/web/src/platform/useWebWorkbenchModel.ts @@ -885,7 +885,7 @@ export function useWebSessionAutoMarkRead( * Map a Hub execution-target inventory row to the Devices page entry (#1819). * Only forwards fields the Hub actually returned (exactOptionalPropertyTypes). */ -export function mapWebExecutionTargetToDeviceEntry( +function mapWebExecutionTargetToDeviceEntry( target: ExecutionTargetInventoryItem, ): DevicesPageTarget { const id = String(target.id ?? ''); @@ -930,6 +930,5 @@ export { type WebExecutionTargetStatus, type WebExecutionTargetStatusState, } from './webWorkbenchExecutionTargets'; -export { hubEmptyContacts as webHubEmptyContacts } from '@agenthub/workbench/hubDataMapping'; export { contactInfoToMember } from '@agenthub/workbench/hubDataMapping'; export { resolveHubContacts as resolveWebWorkbenchContacts } from '@agenthub/workbench/hubDataMapping'; diff --git a/app/web/src/platform/webDeviceKicked.ts b/app/web/src/platform/webDeviceKicked.ts index c2fadc2a9..3cb6f0920 100644 --- a/app/web/src/platform/webDeviceKicked.ts +++ b/app/web/src/platform/webDeviceKicked.ts @@ -14,7 +14,7 @@ import { useHubStore } from '@/stores/hubStore'; import { useToastStore } from '@shared/ui/toast'; /** Minimal translator surface for the kicked feedback copy. */ -export type DeviceKickedTranslator = (key: 'webChat.deviceKicked' | 'webChat.deviceKicked.signIn') => string; +type DeviceKickedTranslator = (key: 'webChat.deviceKicked' | 'webChat.deviceKicked.signIn') => string; /** True when a raw WebSocket frame is the Hub device.kicked frame. */ export function isDeviceKickedFrame(raw: unknown): boolean { diff --git a/app/web/src/platform/webHubRealtime.ts b/app/web/src/platform/webHubRealtime.ts index d1af146b1..d3f46d821 100644 --- a/app/web/src/platform/webHubRealtime.ts +++ b/app/web/src/platform/webHubRealtime.ts @@ -26,7 +26,7 @@ type CreateHubWS = (opts: HubWSOptions) => HubWSHandle; * directly for device.kicked frames (#1816), which the shared hubWS handle * swallows before app-level handlers. */ -export type CreateWebRealtimeTransport = (options: { +type CreateWebRealtimeTransport = (options: { url: string; getToken: () => string | null; }) => Transport; @@ -88,7 +88,7 @@ const TEAM_EVENTS = new Set([ HUB_EVENTS.TEAM_ASSIGNMENT_FAILED, ]); -export interface WebHubRealtimeOptions { +interface WebHubRealtimeOptions { enabled: boolean; runtimeSessionId?: string | null; runtimeTaskId?: string | null; @@ -441,7 +441,7 @@ export const AGENT_STREAM_INVALIDATE_WINDOW_MS = 250; /** One display-frame window for live transcript commits (#1415). */ export const AGENT_STREAM_LIVE_BATCH_WINDOW_MS = 16; -export interface WebWorkbenchHubInvalidationScheduler { +interface WebWorkbenchHubInvalidationScheduler { /** Route one realtime frame: AGENT_STREAM coalesced, everything else immediate. */ notify: (eventType: string, payload: unknown) => void; /** Flush any pending stream invalidation and clear the timer. */ @@ -504,7 +504,7 @@ export function createWebWorkbenchHubInvalidationScheduler( */ type LiveEventFlush = (events: HubRuntimeEventTranscriptInput[]) => void; -export interface WebWorkbenchLiveEventBatcher { +interface WebWorkbenchLiveEventBatcher { /** Queue one stream event; undefined still schedules an activity-only flush. */ push: (event?: HubRuntimeEventTranscriptInput) => void; /** Flush pending work without disposing the batcher. */ diff --git a/app/web/src/platform/webPlatform.ts b/app/web/src/platform/webPlatform.ts index 149989b64..f79947f2f 100644 --- a/app/web/src/platform/webPlatform.ts +++ b/app/web/src/platform/webPlatform.ts @@ -82,7 +82,7 @@ interface SessionAgentInstanceBinding { agentInstance: AgentInstance; } -export interface WebPlatformOptions { +interface WebPlatformOptions { hubClient?: WebRunHubClient; queryClient?: QueryClient; createClientMessageId?: () => string; @@ -269,7 +269,7 @@ export function createWebPlatform(options: WebPlatformOptions = {}): AgentHubPla }; } -export interface SubmitWebComposerIntentOptions { +interface SubmitWebComposerIntentOptions { queryClient?: QueryClient; now?: () => string; } @@ -310,7 +310,7 @@ async function uploadPendingAttachments(intent: ComposerIntent): Promise string, intent: ComposerIntent, @@ -392,7 +392,7 @@ export async function submitWebComposerIntent( * A recoverable 409 turn_in_progress is surfaced as `{ turnInProgress: true }` * so the queue can requeue (bounded) instead of dropping the dispatch. */ -export async function redispatchWebTask( +async function redispatchWebTask( hubClient: WebRunHubClient, messageId: string, intent: ComposerIntent, diff --git a/app/web/src/platform/webPlatformAgentTask.ts b/app/web/src/platform/webPlatformAgentTask.ts index a5bbded89..0d9df4afe 100644 --- a/app/web/src/platform/webPlatformAgentTask.ts +++ b/app/web/src/platform/webPlatformAgentTask.ts @@ -61,7 +61,7 @@ export function readStoredWebActiveAgentTask(sessionId: string): WebActiveAgentT } } -export function writeStoredWebActiveAgentTask(sessionId: string, task: WebActiveAgentTask): void { +function writeStoredWebActiveAgentTask(sessionId: string, task: WebActiveAgentTask): void { if (typeof localStorage === 'undefined') return; localStorage.setItem(webActiveAgentTaskStorageKey(sessionId), JSON.stringify(compactActiveAgentTask(task))); } diff --git a/app/web/src/platform/webPlatformMessageHelpers.ts b/app/web/src/platform/webPlatformMessageHelpers.ts index 0900483ce..cfc65e962 100644 --- a/app/web/src/platform/webPlatformMessageHelpers.ts +++ b/app/web/src/platform/webPlatformMessageHelpers.ts @@ -4,7 +4,7 @@ import type { AttachmentRef, ComposerAttachment, ComposerIntent } from '@shared/ import type { HubContentType } from '@shared/hub/hubClient'; import type { MessageResponse, SendMessageResponse } from '@/api/hubClient'; -export interface ComposerMessageContent { +interface ComposerMessageContent { contentType: HubContentType; content: string; } diff --git a/app/workbench/src/ConversationSidebar.tsx b/app/workbench/src/ConversationSidebar.tsx index 5fbe0d096..9ac8a81ee 100644 --- a/app/workbench/src/ConversationSidebar.tsx +++ b/app/workbench/src/ConversationSidebar.tsx @@ -78,7 +78,7 @@ export interface ConversationSidebarProps { * workbenchProfileChromeHelpers); like those, it is a custom scheme that * consumers may route themselves. */ -export function conversationLinkFor(conversationId: string): string { +function conversationLinkFor(conversationId: string): string { return `agenthub://threads/${conversationId}`; } diff --git a/app/workbench/src/WorkbenchSplitHost.tsx b/app/workbench/src/WorkbenchSplitHost.tsx index 56cb7f73a..c3a6a9fdb 100644 --- a/app/workbench/src/WorkbenchSplitHost.tsx +++ b/app/workbench/src/WorkbenchSplitHost.tsx @@ -18,12 +18,12 @@ import { computeRects, listLeaves, type GroupLeaf, type SplitLayoutNode, type Sp import styles from './AgentHubWorkbench.module.css'; /** Stable React key for the pane shell that hosts the live ConversationHost. */ -export const ACTIVE_PANE_KEY = 'agenthub-split-active-pane'; +const ACTIVE_PANE_KEY = 'agenthub-split-active-pane'; /** Visual gutter between panes (each pane is inset by half from its rect). */ const SPLIT_GAP_PX = 8; -export function splitPaneStyle(rect: SplitRect | undefined): React.CSSProperties { +function splitPaneStyle(rect: SplitRect | undefined): React.CSSProperties { const safe = rect ?? { x: 0, y: 0, w: 1, h: 1 }; const halfGap = SPLIT_GAP_PX / 2; return { @@ -54,7 +54,7 @@ export interface WorkbenchSplitHostProps { * tree position across every layout change; the rest follow in document * order keyed by paneId. */ -export function orderLeavesForRender( +function orderLeavesForRender( tree: SplitLayoutNode, activeConversationId: string, ): GroupLeaf[] { diff --git a/app/workbench/src/__tests__/inspector.test.tsx b/app/workbench/src/__tests__/inspector.test.tsx index c099d415f..ab6ccb18d 100644 --- a/app/workbench/src/__tests__/inspector.test.tsx +++ b/app/workbench/src/__tests__/inspector.test.tsx @@ -336,25 +336,25 @@ describe('AgentHubWorkbench', () => { expect(inspector.getByText('变更文件: 1')).toBeInTheDocument(); expect(inspector.getByText('工具调用: 1')).toBeInTheDocument(); expect( - inspector.getAllByText('app/shared/src/workbench/RightInspector.tsx').length + inspector.getAllByText('app/workbench/src/RightInspector.tsx').length ).toBeGreaterThan(0); expect(inspector.getByText('产物')).toBeInTheDocument(); fireEvent.click( inspector.getByRole('button', { - name: '打开 app/shared/src/workbench/RightInspector.tsx 只读预览', + name: '打开 app/workbench/src/RightInspector.tsx 只读预览', }) ); expect(screen.getByRole('tab', { name: /文件/ })).toHaveAttribute('aria-selected', 'true'); const filePreview = screen.getByRole('region', { - name: 'app/shared/src/workbench/RightInspector.tsx 只读预览', + name: 'app/workbench/src/RightInspector.tsx 只读预览', }); expect(filePreview).toBeInTheDocument(); expect( - screen.getAllByText('app/shared/src/workbench/RightInspector.tsx').length + screen.getAllByText('app/workbench/src/RightInspector.tsx').length ).toBeGreaterThan(0); expect(filePreview).toHaveAccessibleName( - 'app/shared/src/workbench/RightInspector.tsx 只读预览' + 'app/workbench/src/RightInspector.tsx 只读预览' ); fireEvent.click(screen.getByRole('tab', { name: 'Diff' })); fireEvent.click(screen.getByRole('button', { name: /打开方式/ })); @@ -372,7 +372,7 @@ describe('AgentHubWorkbench', () => { expect.objectContaining({ id: 'ev-file', kind: 'file', - label: 'app/shared/src/workbench/RightInspector.tsx', + label: 'app/workbench/src/RightInspector.tsx', }) ); diff --git a/app/workbench/src/agentsProductCopyContract.ts b/app/workbench/src/agentsProductCopyContract.ts index f12ce78f0..b1f33e0b3 100644 --- a/app/workbench/src/agentsProductCopyContract.ts +++ b/app/workbench/src/agentsProductCopyContract.ts @@ -21,10 +21,10 @@ export const BANNED_PRODUCT_EN_META: readonly string[] = [ ] as const; /** Count suffix that Chinese UI expects as "X 个", not "X active". */ -export const BANNED_ACTIVE_COUNT_SUFFIX = /\b\d+\s+active\b/i; +const BANNED_ACTIVE_COUNT_SUFFIX = /\b\d+\s+active\b/i; /** Runtime/Model stuffing patterns that must not land in role/description. */ -export const BANNED_RUNTIME_MODEL_STUFFING = [ +const BANNED_RUNTIME_MODEL_STUFFING = [ /\bRuntime\s*:/i, /\bModel\s*:/i, ] as const; diff --git a/app/workbench/src/floating/ProfilePopoverHelpers.ts b/app/workbench/src/floating/ProfilePopoverHelpers.ts index 407ea953a..0b9acc85e 100644 --- a/app/workbench/src/floating/ProfilePopoverHelpers.ts +++ b/app/workbench/src/floating/ProfilePopoverHelpers.ts @@ -47,9 +47,9 @@ export type ProfilePopoverPosition = { }; export const PROFILE_POPOVER_WIDTH = 352; -export const PROFILE_POPOVER_GAP = 10; -export const PROFILE_POPOVER_EDGE = 12; -export const PROFILE_POPOVER_FALLBACK_HEIGHT = 360; +const PROFILE_POPOVER_GAP = 10; +const PROFILE_POPOVER_EDGE = 12; +const PROFILE_POPOVER_FALLBACK_HEIGHT = 360; /** CSS module variant class for the popover shell. */ export function profileVariantClass( diff --git a/app/workbench/src/floating/ProfilePopoverParts.tsx b/app/workbench/src/floating/ProfilePopoverParts.tsx index e97756668..d14e02eb7 100644 --- a/app/workbench/src/floating/ProfilePopoverParts.tsx +++ b/app/workbench/src/floating/ProfilePopoverParts.tsx @@ -21,7 +21,7 @@ import styles from './ProfilePopover.module.css'; ProfilePopover.module.css. No intentional UX change. ═══════════════════════════════════════════════════════════════════════ */ -export function ProfileAvatarGlyph({ +function ProfileAvatarGlyph({ avatar, avatarColor, avatarUrl, @@ -56,7 +56,7 @@ export function ProfileAvatarGlyph({ ); } -export function ProfileTitleRow({ +function ProfileTitleRow({ name, badge, }: { @@ -71,7 +71,7 @@ export function ProfileTitleRow({ ); } -export function ProfileActionButtons({ +function ProfileActionButtons({ actions, onAction, className, @@ -97,7 +97,7 @@ export function ProfileActionButtons({ ); } -export function ProfileMetaRows({ +function ProfileMetaRows({ meta, }: { meta: ProfileMetaRow[]; @@ -115,7 +115,7 @@ export function ProfileMetaRows({ ); } -export function AccountMenuRows({ +function AccountMenuRows({ accountMenu, onAccountMenu, }: { diff --git a/app/workbench/src/inspector/RuntimeEvidenceParts.tsx b/app/workbench/src/inspector/RuntimeEvidenceParts.tsx index f2cb1bc2b..7ac133c88 100644 --- a/app/workbench/src/inspector/RuntimeEvidenceParts.tsx +++ b/app/workbench/src/inspector/RuntimeEvidenceParts.tsx @@ -41,7 +41,7 @@ export function RuntimeEvidenceEmptyState({ ); } -export function RuntimeEvidenceSection({ +function RuntimeEvidenceSection({ channel, children, count, @@ -68,7 +68,7 @@ export function RuntimeEvidenceSection({ ); } -export function ArtifactWorkspaceProjection({ +function ArtifactWorkspaceProjection({ artifact, diffCount, evidenceSourceLabel, diff --git a/app/workbench/src/pages/agents/AgentOpsItemParts.tsx b/app/workbench/src/pages/agents/AgentOpsItemParts.tsx index ee3fc5454..4058ceff7 100644 --- a/app/workbench/src/pages/agents/AgentOpsItemParts.tsx +++ b/app/workbench/src/pages/agents/AgentOpsItemParts.tsx @@ -165,7 +165,7 @@ export const CcSwitchStatusGrid: React.FC<{ ); }; -export const CcSwitchProviderCard: React.FC<{ +const CcSwitchProviderCard: React.FC<{ provider: CCSwitchProviderInfo; }> = ({ provider }) => { const { t } = useTranslation(SHARED_WORKBENCH_I18N_NAMESPACE); diff --git a/app/workbench/src/pages/projects/ProjectPanelParts.tsx b/app/workbench/src/pages/projects/ProjectPanelParts.tsx index b85df0922..c5d3e01be 100644 --- a/app/workbench/src/pages/projects/ProjectPanelParts.tsx +++ b/app/workbench/src/pages/projects/ProjectPanelParts.tsx @@ -50,7 +50,7 @@ export function MembersCard({ ); } -export function ProjectProfilePill({ +function ProjectProfilePill({ name, profiles = [], }: { diff --git a/app/workbench/src/pages/tasks/TaskTableParts.tsx b/app/workbench/src/pages/tasks/TaskTableParts.tsx index b4b857035..bd221f48c 100644 --- a/app/workbench/src/pages/tasks/TaskTableParts.tsx +++ b/app/workbench/src/pages/tasks/TaskTableParts.tsx @@ -135,7 +135,7 @@ export function TaskStatusIcon({ status }: { status: TaskStatus }) { return ; } -export function ProfileCell({ +function ProfileCell({ name, profiles = [], }: { diff --git a/app/workbench/src/unifiedComposerHelpers.ts b/app/workbench/src/unifiedComposerHelpers.ts index 463404eb1..18f636755 100644 --- a/app/workbench/src/unifiedComposerHelpers.ts +++ b/app/workbench/src/unifiedComposerHelpers.ts @@ -86,14 +86,14 @@ export function isTargetSelectionRequired( return Boolean(executionTargets) && mentions.length > 0; } -export function isExecutionTargetSelected( +function isExecutionTargetSelected( targetSelectionRequired: boolean, executionTargetId: string, ): boolean { return !targetSelectionRequired || executionTargetId.trim().length > 0; } -export function resolveSelectedTargetLabel( +function resolveSelectedTargetLabel( executionTargets: ComposerExecutionTarget[] | undefined, executionTargetId: string, ): string | undefined { @@ -178,7 +178,7 @@ export function buildComposerStatusItems(params: { ].filter((item): item is string => Boolean(item)); } -export function isComposerSubmitDisabled(params: { +function isComposerSubmitDisabled(params: { composer: ComposerState; isSubmitting: boolean; targetSelected: boolean; diff --git a/app/workbench/src/workbenchAgentMapping.ts b/app/workbench/src/workbenchAgentMapping.ts index a1263c875..44632849c 100644 --- a/app/workbench/src/workbenchAgentMapping.ts +++ b/app/workbench/src/workbenchAgentMapping.ts @@ -46,7 +46,7 @@ export function workbenchAgentStateToAgentState(status: WorkbenchAgent['status'] } } -export function toolPermissionFromAgent(agent: WorkbenchAgent): Record { +function toolPermissionFromAgent(agent: WorkbenchAgent): Record { const allowedTools = new Set(agent.toolAllowlist ?? []); if (allowedTools.size === 0) return {}; return Object.fromEntries( diff --git a/app/workbench/src/workbenchAgentsRouteHelpers.ts b/app/workbench/src/workbenchAgentsRouteHelpers.ts index 9329089ad..8e56383b8 100644 --- a/app/workbench/src/workbenchAgentsRouteHelpers.ts +++ b/app/workbench/src/workbenchAgentsRouteHelpers.ts @@ -224,7 +224,7 @@ export function withoutAgentDirty(dirtyAgentIds: string[], agentId: string): str return dirtyAgentIds.filter((id) => id !== agentId); } -export function patchAgentDrafts( +function patchAgentDrafts( drafts: Record, agentId: string, current: AgentConfig, diff --git a/app/workbench/src/workbenchApprovalEvents.ts b/app/workbench/src/workbenchApprovalEvents.ts index 51e496c9e..6b014bb8f 100644 --- a/app/workbench/src/workbenchApprovalEvents.ts +++ b/app/workbench/src/workbenchApprovalEvents.ts @@ -7,7 +7,3 @@ * persisted state — a late event with no listener is simply dropped. */ export const WORKBENCH_APPROVAL_JUMP_EVENT = 'agenthub:approval-jump'; - -export interface ApprovalJumpDetail { - conversationId: string; -} diff --git a/app/workbench/src/workbenchDataMode.ts b/app/workbench/src/workbenchDataMode.ts index 4cd5c980e..ea6aabfbc 100644 --- a/app/workbench/src/workbenchDataMode.ts +++ b/app/workbench/src/workbenchDataMode.ts @@ -43,7 +43,7 @@ export interface WorkbenchSectionSourceInput { hasLocalDryRun?: boolean; } -export const workbenchDataModeLabels: Record = { +const workbenchDataModeLabels: Record = { loading: 'Loading catalog', live: 'Live', 'offline-snapshot': 'Offline snapshot', @@ -51,7 +51,7 @@ export const workbenchDataModeLabels: Record = { unavailable: 'Snapshot unavailable', }; -export const workbenchDataModeTones: Record = { +const workbenchDataModeTones: Record = { loading: 'cyan', live: 'green', 'offline-snapshot': 'purple', diff --git a/app/workbench/src/workbenchSplitLayout.ts b/app/workbench/src/workbenchSplitLayout.ts index ccfe36f41..cba3f15ea 100644 --- a/app/workbench/src/workbenchSplitLayout.ts +++ b/app/workbench/src/workbenchSplitLayout.ts @@ -55,7 +55,7 @@ const MAX_VALIDATION_DEPTH = 32; let paneIdCounter = 0; /** Deterministic-enough unique pane id; explicit ids may be injected in tests. */ -export function generatePaneId(): string { +function generatePaneId(): string { paneIdCounter += 1; return `split-pane-${Date.now().toString(36)}-${paneIdCounter}`; } diff --git a/app/workbench/src/workbenchSplitTranscriptCache.ts b/app/workbench/src/workbenchSplitTranscriptCache.ts index 9cb07cfef..d58804b88 100644 --- a/app/workbench/src/workbenchSplitTranscriptCache.ts +++ b/app/workbench/src/workbenchSplitTranscriptCache.ts @@ -13,7 +13,7 @@ import { useEffect, useState } from 'react'; import type { TranscriptBlock } from '@shared/transcript'; /** Bound the cache: a handful of inactive panes is the realistic maximum. */ -export const SPLIT_TRANSCRIPT_CACHE_LIMIT = 8; +const SPLIT_TRANSCRIPT_CACHE_LIMIT = 8; /** * Snapshot the active conversation's transcript on every change. Returns a diff --git a/app/workbench/src/workbenchTaskDeepLinks.ts b/app/workbench/src/workbenchTaskDeepLinks.ts index ea6f7d621..c1bc77283 100644 --- a/app/workbench/src/workbenchTaskDeepLinks.ts +++ b/app/workbench/src/workbenchTaskDeepLinks.ts @@ -99,7 +99,7 @@ function sameTaskQueue(a: readonly TaskItem[], b: readonly TaskItem[]): boolean } /** Create one isolated store. Production creates exactly one per Workbench mount. */ -export function createWorkbenchTaskDeepLinkStore(): WorkbenchTaskDeepLinkStore { +function createWorkbenchTaskDeepLinkStore(): WorkbenchTaskDeepLinkStore { let snapshot = createInitialSnapshot(); let focusSeq = 0; const listeners = new Set<() => void>(); diff --git a/app/workbench/src/workbenchTaskGroups.ts b/app/workbench/src/workbenchTaskGroups.ts index d5da2053c..55e8b5ebb 100644 --- a/app/workbench/src/workbenchTaskGroups.ts +++ b/app/workbench/src/workbenchTaskGroups.ts @@ -24,8 +24,8 @@ export const DESIGN_DONE_TASK: TaskItem = { status: '已完成', }; -export const WATCHING_TASK_IDS = new Set(['embedded-docs', 'project-announcement']); -export const ACTIVITY_TASK_IDS = new Set(['sqlite-plan', 'project-announcement', 'agent-market']); +const WATCHING_TASK_IDS = new Set(['embedded-docs', 'project-announcement']); +const ACTIVITY_TASK_IDS = new Set(['sqlite-plan', 'project-announcement', 'agent-market']); export function flattenTaskGroups(groups: TaskGroup[]): TaskItem[] { return groups.flatMap((group) => group.tasks); @@ -50,7 +50,7 @@ export function taskMatchesPane(task: TaskItem, pane: TasksPane, currentUserId?: } } -export function dueRank(label: string): number { +function dueRank(label: string): number { if (label.includes('今天')) return 0; if (label.includes('明天')) return 1; const match = /(\d+)月(\d+)日/.exec(label); diff --git a/app/workbench/src/workbenchTestFixtures.ts b/app/workbench/src/workbenchTestFixtures.ts index 3f45aea6e..712efd5f6 100644 --- a/app/workbench/src/workbenchTestFixtures.ts +++ b/app/workbench/src/workbenchTestFixtures.ts @@ -120,10 +120,10 @@ export const workbenchTranscript: TranscriptBlock[] = [ id: 'diff-1', kind: 'diff', author: { id: 'builder', name: 'Builder', role: 'agent' }, - title: 'app/shared/src/workbench/RightInspector.tsx', - files: ['app/shared/src/workbench/RightInspector.tsx'], + title: 'app/workbench/src/RightInspector.tsx', + files: ['app/workbench/src/RightInspector.tsx'], evidenceRefs: [ - { id: 'ev-file', kind: 'file', label: 'app/shared/src/workbench/RightInspector.tsx' }, + { id: 'ev-file', kind: 'file', label: 'app/workbench/src/RightInspector.tsx' }, ], }, {