Skip to content
Draft
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
10 changes: 9 additions & 1 deletion desktop/src/app/AppShell.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ export type AppView =
| "agents"
| "workflows"
| "pulse"
| "projects";
| "projects"
| "radar";

const WINDOW_DRAG_HANDLE_HEIGHT = 44;
const TAURI_DRAG_REGION_ATTR = "data-tauri-drag-region";
Expand Down Expand Up @@ -153,6 +154,13 @@ export function deriveShellRoute(pathname: string): {
};
}

if (pathname === "/radar") {
return {
selectedChannelId: null,
selectedView: "radar",
};
}

return {
selectedChannelId: null,
selectedView: "home",
Expand Down
2 changes: 2 additions & 0 deletions desktop/src/app/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ export function AppShell() {
goNewMessage,
goProjects,
goPulse,
goRadar,
goSettings,
goWorkflows,
closeSettings,
Expand Down Expand Up @@ -881,6 +882,7 @@ export function AppShell() {
onSelectHome={() => void goHome()}
onSelectProjects={() => void goProjects()}
onSelectPulse={() => void goPulse()}
onSelectRadar={() => void goRadar()}
onSelectSettings={handleOpenSettings}
onSelectWorkflows={() => void goWorkflows()}
onSetPresenceStatus={(status) =>
Expand Down
12 changes: 12 additions & 0 deletions desktop/src/app/navigation/useAppNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,17 @@ export function useAppNavigation() {
[commitNavigation],
);

const goRadar = React.useCallback(
(behavior?: NavigationBehavior) =>
commitNavigation(
{
to: "/radar",
},
behavior,
),
[commitNavigation],
);

const goProjects = React.useCallback(
(behavior?: NavigationBehavior) =>
commitNavigation(
Expand Down Expand Up @@ -303,6 +314,7 @@ export function useAppNavigation() {
goProject,
goProjects,
goPulse,
goRadar,
goSettings,
goWorkflow,
goWorkflows,
Expand Down
21 changes: 21 additions & 0 deletions desktop/src/app/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Route as rootRouteImport } from "./routes/root";
import { Route as workflowsRouteImport } from "./routes/workflows";
import { Route as settingsRouteImport } from "./routes/settings";
import { Route as remindersRouteImport } from "./routes/reminders";
import { Route as radarRouteImport } from "./routes/radar";
import { Route as pulseRouteImport } from "./routes/pulse";
import { Route as projectsRouteImport } from "./routes/projects";
import { Route as agentsRouteImport } from "./routes/agents";
Expand All @@ -33,6 +34,11 @@ const remindersRoute = remindersRouteImport.update({
path: "/reminders",
getParentRoute: () => rootRouteImport,
} as any);
const radarRoute = radarRouteImport.update({
id: "/radar",
path: "/radar",
getParentRoute: () => rootRouteImport,
} as any);
const pulseRoute = pulseRouteImport.update({
id: "/pulse",
path: "/pulse",
Expand Down Expand Up @@ -85,6 +91,7 @@ export interface FileRoutesByFullPath {
"/agents": typeof agentsRoute;
"/projects": typeof projectsRoute;
"/pulse": typeof pulseRoute;
"/radar": typeof radarRoute;
"/reminders": typeof remindersRoute;
"/settings": typeof settingsRoute;
"/workflows": typeof workflowsRoute;
Expand All @@ -99,6 +106,7 @@ export interface FileRoutesByTo {
"/agents": typeof agentsRoute;
"/projects": typeof projectsRoute;
"/pulse": typeof pulseRoute;
"/radar": typeof radarRoute;
"/reminders": typeof remindersRoute;
"/settings": typeof settingsRoute;
"/workflows": typeof workflowsRoute;
Expand All @@ -114,6 +122,7 @@ export interface FileRoutesById {
"/agents": typeof agentsRoute;
"/projects": typeof projectsRoute;
"/pulse": typeof pulseRoute;
"/radar": typeof radarRoute;
"/reminders": typeof remindersRoute;
"/settings": typeof settingsRoute;
"/workflows": typeof workflowsRoute;
Expand All @@ -130,6 +139,7 @@ export interface FileRouteTypes {
| "/agents"
| "/projects"
| "/pulse"
| "/radar"
| "/reminders"
| "/settings"
| "/workflows"
Expand All @@ -144,6 +154,7 @@ export interface FileRouteTypes {
| "/agents"
| "/projects"
| "/pulse"
| "/radar"
| "/reminders"
| "/settings"
| "/workflows"
Expand All @@ -158,6 +169,7 @@ export interface FileRouteTypes {
| "/agents"
| "/projects"
| "/pulse"
| "/radar"
| "/reminders"
| "/settings"
| "/workflows"
Expand All @@ -173,6 +185,7 @@ export interface RootRouteChildren {
agentsRoute: typeof agentsRoute;
projectsRoute: typeof projectsRoute;
pulseRoute: typeof pulseRoute;
radarRoute: typeof radarRoute;
remindersRoute: typeof remindersRoute;
settingsRoute: typeof settingsRoute;
workflowsRoute: typeof workflowsRoute;
Expand Down Expand Up @@ -206,6 +219,13 @@ declare module "@tanstack/react-router" {
preLoaderRoute: typeof remindersRouteImport;
parentRoute: typeof rootRouteImport;
};
"/radar": {
id: "/radar";
path: "/radar";
fullPath: "/radar";
preLoaderRoute: typeof radarRouteImport;
parentRoute: typeof rootRouteImport;
};
"/pulse": {
id: "/pulse";
path: "/pulse";
Expand Down Expand Up @@ -277,6 +297,7 @@ const rootRouteChildren: RootRouteChildren = {
agentsRoute: agentsRoute,
projectsRoute: projectsRoute,
pulseRoute: pulseRoute,
radarRoute: radarRoute,
remindersRoute: remindersRoute,
settingsRoute: settingsRoute,
workflowsRoute: workflowsRoute,
Expand Down
1 change: 1 addition & 0 deletions desktop/src/app/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export const routes = rootRoute("root.tsx", [
index("index.tsx"),
route("/agents", "agents.tsx"),
route("/pulse", "pulse.tsx"),
route("/radar", "radar.tsx"),
route("/reminders", "reminders.tsx"),
route("/settings", "settings.tsx"),
route("/workflows", "workflows.tsx"),
Expand Down
23 changes: 23 additions & 0 deletions desktop/src/app/routes/radar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import * as React from "react";
import { createFileRoute } from "@tanstack/react-router";

import { usePreviewFeatureWarning } from "@/shared/features";
import { ViewLoadingFallback } from "@/shared/ui/ViewLoadingFallback";

const RadarScreen = React.lazy(async () => {
const module = await import("@/features/radar/ui/RadarScreen");
return { default: module.RadarScreen };
});

export const Route = createFileRoute("/radar")({
component: RadarRouteComponent,
});

function RadarRouteComponent() {
usePreviewFeatureWarning("radar");
return (
<React.Suspense fallback={<ViewLoadingFallback kind="workflows" />}>
<RadarScreen />
</React.Suspense>
);
}
118 changes: 118 additions & 0 deletions desktop/src/features/radar/hooks/useRadarStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import * as React from "react";

import type {
RadarFinding,
RadarSnapshot,
} from "@/features/radar/lib/radarTypes";

const BUZZ_COMMITS_URL =
"https://api.github.com/repos/dschwartzAI/buzz/commits?sha=main&per_page=20";

type GithubCommit = {
sha?: string;
html_url?: string;
commit?: {
message?: string;
author?: { date?: string | null };
};
};

function firstUsefulSentence(value: string | null | undefined): string | null {
const normalized = value
?.replace(/<!--[\s\S]*?-->/g, "")
.replace(/[#*_`>\r\n]+/g, " ")
.replace(/\s+/g, " ")
.trim();
if (!normalized) return null;
const sentence = normalized.match(/^.*?[.!?](?:\s|$)/)?.[0] ?? normalized;
return sentence.slice(0, 240);
}

function findingsFromMainCommits(value: unknown): RadarFinding[] {
if (!Array.isArray(value)) return [];

return value.flatMap((candidate) => {
const entry = candidate as GithubCommit;
const message = entry.commit?.message?.trim();
const title = message?.split("\n", 1)[0]?.trim();
const date = entry.commit?.author?.date;
if (
typeof entry.sha !== "string" ||
typeof entry.html_url !== "string" ||
!title ||
typeof date !== "string"
) {
return [];
}

const detail = message?.slice(title.length).trim();
return [
{
id: `buzz-commit-${entry.sha}`,
url: entry.html_url,
summary: title,
whyItMatters:
firstUsefulSentence(detail) ?? "This shipped on Buzz's main branch.",
chiefsTake: null,
foundAt: date,
source: "buzz_update" as const,
},
];
});
}

async function fetchRadarSnapshot(): Promise<RadarSnapshot> {
const response = await fetch(BUZZ_COMMITS_URL, {
headers: { Accept: "application/vnd.github+json" },
});
if (!response.ok) {
throw new Error(`Buzz updates returned HTTP ${response.status}`);
}

return {
status: {
state: "waiting_for_x_access",
lastScanAt: null,
nextScanAt: null,
lastError: null,
},
sources: [
{ id: "buzz-main", label: "Buzz updates merged to main" },
{ id: "x-watch", label: "X watch (waiting for enrolled API access)" },
],
findings: findingsFromMainCommits(await response.json()),
};
}

export function useRadarStatus(): {
snapshot: RadarSnapshot | null;
error: string | null;
refresh: () => void;
} {
const [snapshot, setSnapshot] = React.useState<RadarSnapshot | null>(null);
const [error, setError] = React.useState<string | null>(null);

const fetchOnce = React.useCallback(() => {
let cancelled = false;
(async () => {
try {
const value = await fetchRadarSnapshot();
if (!cancelled) {
setSnapshot(value);
setError(null);
}
} catch (err) {
if (!cancelled) {
setError(err instanceof Error ? err.message : String(err));
}
}
})();
return () => {
cancelled = true;
};
}, []);

React.useEffect(() => fetchOnce(), [fetchOnce]);

return { snapshot, error, refresh: fetchOnce };
}
31 changes: 31 additions & 0 deletions desktop/src/features/radar/lib/radarTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export type RadarRunState = "running" | "paused" | "waiting_for_x_access";

export type RadarStatus = {
state: RadarRunState;
lastScanAt: string | null;
nextScanAt: string | null;
lastError: string | null;
};

export type RadarSource = {
id: string;
label: string;
};

export type RadarFindingSource = "buzz_update" | "x_watch";

export type RadarFinding = {
id: string;
url: string;
summary: string;
whyItMatters: string;
chiefsTake: string | null;
foundAt: string;
source: RadarFindingSource;
};

export type RadarSnapshot = {
status: RadarStatus;
sources: RadarSource[];
findings: RadarFinding[];
};
18 changes: 18 additions & 0 deletions desktop/src/features/radar/ui/RadarScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as React from "react";

import { ViewLoadingFallback } from "@/shared/ui/ViewLoadingFallback";

const RadarView = React.lazy(async () => {
const module = await import("@/features/radar/ui/RadarView");
return { default: module.RadarView };
});

export function RadarScreen() {
return (
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
<React.Suspense fallback={<ViewLoadingFallback kind="workflows" />}>
<RadarView />
</React.Suspense>
</div>
);
}
Loading