Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions apps/web/src/command-palette-actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]);
});
});
12 changes: 1 addition & 11 deletions apps/web/src/command-palette-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -148,9 +142,5 @@ export async function runActionCommand(
ctx.navigate(WORKBENCH_PATH_PREFIX);
return;
}
case "go-insights": {
ctx.navigate("/insights");
return;
}
}
}
40 changes: 40 additions & 0 deletions apps/web/src/insights-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>)["error"];
if (typeof error !== "object" || error === null) continue;
const message = (error as Record<string, unknown>)["message"];
if (typeof message === "string") return message;
}
return null;
}
97 changes: 93 additions & 4 deletions apps/web/src/pages/insights-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 <Skeleton className="h-24 w-full" />;
if (events.kind !== "ready") {
return (
<RichEmptyState
title="Couldn't load this run's events"
description="Something went wrong on our side. Try again in a moment."
{...(events.kind === "error"
? { actions: [{ label: "Retry", onClick: events.retry }] }
: {})}
/>
);
}

const message = runFailureMessage(events.data.events);

return (
<section className="insights-panel">
<h3>Failure</h3>
<p className="text-sm text-destructive">
{message ?? "This run failed, but no event carried a specific error message."}
</p>
<button
type="button"
className="font-semibold text-primary-emphasis"
onClick={() => setShowEvents((value) => !value)}
>
{showEvents ? "Hide events" : "View events"}
</button>
{showEvents ? (
<Table aria-label="Run events" className="insights-data-table">
<TableHeader>
<TableRow>
<TableHead>Seq</TableHead>
<TableHead>Type</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{events.data.events.map((event) => (
<TableRow key={event.seq}>
<TableCell>{event.seq}</TableCell>
<TableCell>{event.type}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
) : null}
</section>
);
}

export function InsightsRunDetail({
run,
tenantId,
}: {
readonly run: InsightsRun | null;
readonly tenantId: string | null;
}) {
const failed = run !== null && insightsRunStatus(run) === "failed";
return (
<div className="flex h-full min-h-0 flex-col">
<StageTopBar
Expand Down Expand Up @@ -526,6 +603,9 @@ export function InsightsRunDetail({ run }: { readonly run: InsightsRun | null })
description="This run may have fallen out of the 100 most recent, or it never existed."
/>
) : null}
{failed && tenantId !== null ? (
<RunFailureDetail tenantId={tenantId} runId={run.id} />
) : null}
</div>
</PageShell>
</div>
Expand All @@ -537,13 +617,15 @@ export function InsightsPage({
path,
runs,
routines,
tenantId = null,
}: {
readonly path: string;
readonly runs: APIQuery<{
data: readonly InsightsRun[];
nextCursor: string | null;
}>;
readonly routines: APIQuery<readonly ScheduledWorkflowDefinition[]>;
readonly tenantId?: string | null;
}) {
const navigate = useNavigate();
const { mode, runId } = parseInsightsPath(path);
Expand All @@ -569,7 +651,7 @@ export function InsightsPage({

if (mode === "run" && runId !== null) {
const run = runsData.find((r) => r.id === runId) ?? null;
return <InsightsRunDetail run={run} />;
return <InsightsRunDetail run={run} tenantId={tenantId} />;
}

if (mode === "runs") {
Expand Down Expand Up @@ -728,7 +810,14 @@ export function InsightsRoute({ path }: { readonly path?: string }) {
);
}

return <InsightsPage path={currentPath} runs={runsForPage} routines={routinesForPage} />;
return (
<InsightsPage
path={currentPath}
runs={runsForPage}
routines={routinesForPage}
tenantId={selectedTenantId}
/>
);
}

/** Resolves the workbench-scoped route's own workbench list — split out of
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/pages/tools-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ export function ToolsPage({ tenantId }: { readonly tenantId: string | null }) {
/>
) : (
<div className="px-4 pb-5 sm:px-7">
<p className="mb-3 text-sm text-muted-foreground">
Packages are read-only here — deploy an agent to add or change one.
</p>
<Table aria-label="Tools">
<TableHeader>
<TableRow>
Expand Down
24 changes: 3 additions & 21 deletions apps/web/src/settings/account-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -50,7 +50,6 @@ export function AccountSection({ onSignOut }: { readonly onSignOut?: () => void
)}
</QueryView>
<AppearanceSection />
<AgentGeneralSection />
</>
);
}
Expand Down Expand Up @@ -188,20 +187,3 @@ export function AppearanceSection() {
</SettingsPanel>
);
}

/** 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 (
<SettingsPanel title={SETTINGS_STRINGS.generalAgentGroupTitle}>
<div className="settings-form-field settings-form-field-inline">
<span>{SETTINGS_STRINGS.agentTimezoneLabel}</span>
<span className="settings-static-value">
{SETTINGS_STRINGS.agentTimezoneAutoDetect(timezone)}
</span>
</div>
</SettingsPanel>
);
}
7 changes: 1 addition & 6 deletions apps/web/src/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
4 changes: 0 additions & 4 deletions apps/web/src/settings/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 0 additions & 6 deletions apps/web/src/shell/first-run-tour.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading