diff --git a/apps/web/src/command-palette-actions.test.ts b/apps/web/src/command-palette-actions.test.ts index 2f3734bd8..0d7d05dcf 100644 --- a/apps/web/src/command-palette-actions.test.ts +++ b/apps/web/src/command-palette-actions.test.ts @@ -136,10 +136,4 @@ describe("runActionCommand", () => { await runActionCommand("go-workbenches", ctx); expect(navigated).toEqual(["/w"]); }); - - test("go-insights navigates to /insights", async () => { - const { ctx, navigated } = context({ path: "/" }); - await runActionCommand("go-insights", ctx); - expect(navigated).toEqual(["/insights"]); - }); }); diff --git a/apps/web/src/command-palette-actions.ts b/apps/web/src/command-palette-actions.ts index 8b627d1c1..803b17363 100644 --- a/apps/web/src/command-palette-actions.ts +++ b/apps/web/src/command-palette-actions.ts @@ -44,8 +44,7 @@ export type ActionCommandId = | "toggle-theme" | "close-canvas" | "talk-to-myra" - | "go-workbenches" - | "go-insights"; + | "go-workbenches"; export type ActionCommand = { readonly id: ActionCommandId; @@ -85,11 +84,6 @@ export const ACTION_COMMANDS: readonly ActionCommand[] = [ title: "Go to workbenches", subtitle: "Home · conversation list", }, - { - id: "go-insights", - title: "Go to insights", - subtitle: "Settings · still routable", - }, ]; export type ActionCommandContext = { @@ -148,9 +142,5 @@ export async function runActionCommand( ctx.navigate(WORKBENCH_PATH_PREFIX); return; } - case "go-insights": { - ctx.navigate("/insights"); - return; - } } } diff --git a/apps/web/src/insights-api.ts b/apps/web/src/insights-api.ts index 11c6c2f24..1adebbdf4 100644 --- a/apps/web/src/insights-api.ts +++ b/apps/web/src/insights-api.ts @@ -77,3 +77,43 @@ const TOP_LEVEL_RUNS_LIMIT = 100; export function insightsTopLevelRunsPath(tenantId: string): string { return `/api/tenants/${tenantId}/workflows/runs?limit=${TOP_LEVEL_RUNS_LIMIT}`; } + +/** One run's event log, from the stock + * `GET /workflows/runs/:runId/events` route. `body` is left unparsed + * (`unknown`) since its shape varies by event type — `runFailureMessage` + * below is the one place that reaches into it. */ +export const RunEventSchema = type({ + seq: "number", + type: "string", + body: "unknown", +}); + +export const RunEventsSchema = type({ + runId: "string", + events: RunEventSchema.array(), +}); +export type RunEvent = typeof RunEventSchema.infer; +export type RunEvents = typeof RunEventsSchema.infer; + +export function insightsRunEventsPath(tenantId: string, runId: string): string { + return `/api/tenants/${tenantId}/workflows/runs/${encodeURIComponent(runId)}/events`; +} + +/** The failed run's own explanation: the last `RunFailed`/`StepFailed` + * event's `error.message`, read defensively since `body` is unparsed. Null + * when no failure event carries a message — the caller falls back to a + * generic notice rather than showing nothing. */ +export function runFailureMessage(events: readonly RunEvent[]): string | null { + for (let i = events.length - 1; i >= 0; i--) { + const event = events[i]; + if (event === undefined) continue; + if (event.type !== "RunFailed" && event.type !== "StepFailed") continue; + const body = event.body; + if (typeof body !== "object" || body === null) continue; + const error = (body as Record)["error"]; + if (typeof error !== "object" || error === null) continue; + const message = (error as Record)["message"]; + if (typeof message === "string") return message; + } + return null; +} diff --git a/apps/web/src/pages/insights-page.tsx b/apps/web/src/pages/insights-page.tsx index 1d0ecfb20..8d5308d9b 100644 --- a/apps/web/src/pages/insights-page.tsx +++ b/apps/web/src/pages/insights-page.tsx @@ -38,7 +38,14 @@ import { workbenchesQueryKey, listWorkbenches } from "@/chat/workbench-tenants"; import { useBench } from "../bench-context"; import { resolveWorkbenchInsightsScope } from "../insights-workbench-scope"; import { parseInsightsPath } from "../insights-path"; -import { insightsTopLevelRunsPath, TopLevelRunsSchema, type InsightsRun } from "../insights-api"; +import { + insightsTopLevelRunsPath, + insightsRunEventsPath, + RunEventsSchema, + runFailureMessage, + TopLevelRunsSchema, + type InsightsRun, +} from "../insights-api"; import { computeInsightsStats, durationLabel, @@ -496,7 +503,77 @@ export function InsightsRunsHistory({ ); } -export function InsightsRunDetail({ run }: { readonly run: InsightsRun | null }) { +/** The failed run's own explanation plus a link to its full event log, + * fetched from the stock `GET .../runs/:runId/events` route only for a + * failed run — a healthy run has nothing to explain. */ +function RunFailureDetail({ + tenantId, + runId, +}: { + readonly tenantId: string; + readonly runId: string; +}) { + const events = useAPIQuery(insightsRunEventsPath(tenantId, runId), RunEventsSchema); + const [showEvents, setShowEvents] = useState(false); + + if (events.kind === "loading") return ; + if (events.kind !== "ready") { + return ( + + ); + } + + const message = runFailureMessage(events.data.events); + + return ( +
+

Failure

+

+ {message ?? "This run failed, but no event carried a specific error message."} +

+ + {showEvents ? ( + + + + Seq + Type + + + + {events.data.events.map((event) => ( + + {event.seq} + {event.type} + + ))} + +
+ ) : null} +
+ ); +} + +export function InsightsRunDetail({ + run, + tenantId, +}: { + readonly run: InsightsRun | null; + readonly tenantId: string | null; +}) { + const failed = run !== null && insightsRunStatus(run) === "failed"; return (
) : null} + {failed && tenantId !== null ? ( + + ) : null}
@@ -537,6 +617,7 @@ export function InsightsPage({ path, runs, routines, + tenantId = null, }: { readonly path: string; readonly runs: APIQuery<{ @@ -544,6 +625,7 @@ export function InsightsPage({ nextCursor: string | null; }>; readonly routines: APIQuery; + readonly tenantId?: string | null; }) { const navigate = useNavigate(); const { mode, runId } = parseInsightsPath(path); @@ -569,7 +651,7 @@ export function InsightsPage({ if (mode === "run" && runId !== null) { const run = runsData.find((r) => r.id === runId) ?? null; - return ; + return ; } if (mode === "runs") { @@ -728,7 +810,14 @@ export function InsightsRoute({ path }: { readonly path?: string }) { ); } - return ; + return ( + + ); } /** Resolves the workbench-scoped route's own workbench list — split out of diff --git a/apps/web/src/pages/tools-page.tsx b/apps/web/src/pages/tools-page.tsx index 0aa22c6de..923a822bf 100644 --- a/apps/web/src/pages/tools-page.tsx +++ b/apps/web/src/pages/tools-page.tsx @@ -59,6 +59,9 @@ export function ToolsPage({ tenantId }: { readonly tenantId: string | null }) { /> ) : (
+

+ Packages are read-only here — deploy an agent to add or change one. +

diff --git a/apps/web/src/settings/account-section.tsx b/apps/web/src/settings/account-section.tsx index 7ce720376..218efef03 100644 --- a/apps/web/src/settings/account-section.tsx +++ b/apps/web/src/settings/account-section.tsx @@ -3,9 +3,9 @@ // readout as before tucked below as a quieter subsection (still read-only — // there is no native profile-update route; see `vendor/intx/hub-api/src/ // routes` — only tenants and principals carry a PATCH), an Appearance card -// wired to `@corbits/react-ui`'s three-state ThemeProvider, and an Agent -// card whose Timezone row is display-only until a hub preference store -// exists to write it to. +// wired to `@corbits/react-ui`'s three-state ThemeProvider. The Agent card +// (a display-only Timezone row) was removed — nothing there could be +// changed until a hub preference store exists to write it to. import { Avatar, @@ -50,7 +50,6 @@ export function AccountSection({ onSignOut }: { readonly onSignOut?: () => void )} - ); } @@ -188,20 +187,3 @@ export function AppearanceSection() { ); } - -/** Timezone row: display-only, derived from the browser — there is no hub - * preference store yet for a per-user timezone override, so this shows the - * auto-detected zone honestly instead of a dropdown that saves nothing. */ -export function AgentGeneralSection() { - const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; - return ( - -
- {SETTINGS_STRINGS.agentTimezoneLabel} - - {SETTINGS_STRINGS.agentTimezoneAutoDetect(timezone)} - -
-
- ); -} diff --git a/apps/web/src/settings/index.ts b/apps/web/src/settings/index.ts index fd325b8a8..f60e17c54 100644 --- a/apps/web/src/settings/index.ts +++ b/apps/web/src/settings/index.ts @@ -3,12 +3,7 @@ export type { SettingsContext, SettingsSection, SettingsSectionGroup } from "./s export { resolveSettingsSectionGroups, insertEveryoneSections } from "./section-registry"; -export { - AccountSection, - AccountSectionView, - AppearanceSection, - AgentGeneralSection, -} from "./account-section"; +export { AccountSection, AccountSectionView, AppearanceSection } from "./account-section"; // NotificationsSection is not exported: it is draft-only and not in the // registry. Re-export when a preference store backs it. export { AuditSection } from "./audit-section"; diff --git a/apps/web/src/settings/strings.ts b/apps/web/src/settings/strings.ts index ba2d0ff09..fc7ad7829 100644 --- a/apps/web/src/settings/strings.ts +++ b/apps/web/src/settings/strings.ts @@ -46,10 +46,6 @@ export const SETTINGS_STRINGS = { themeLight: "Light", themeDark: "Dark", - generalAgentGroupTitle: "Agent", - agentTimezoneLabel: "Timezone", - agentTimezoneAutoDetect: (timezone: string) => `Auto-detect (${timezone})`, - peopleSectionTitle: "People", peopleSectionDescription: "Everyone with a seat on this workbench.", peopleLoadError: "this workbench's people", diff --git a/apps/web/src/shell/first-run-tour.tsx b/apps/web/src/shell/first-run-tour.tsx index 7b97be484..c71e7b499 100644 --- a/apps/web/src/shell/first-run-tour.tsx +++ b/apps/web/src/shell/first-run-tour.tsx @@ -42,12 +42,6 @@ const STEPS: readonly Step[] = [ content: "Spin up a new workbench whenever you need a fresh conversation.", placement: "bottom", }, - { - target: '[data-tour="composer"]', - title: "Talk to your agent", - content: "Type here to send a message, share files, or @mention another agent.", - placement: "top", - }, { target: '[data-tour="settings-button"]', title: "Settings",