@skyhook-io/radar-app 1.9.0 → 1.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -7
- package/src/App.tsx +69 -16
- package/src/api/apiResources.test.ts +11 -0
- package/src/api/apiResources.ts +51 -12
- package/src/api/client.capacity.test.ts +92 -0
- package/src/api/client.ts +2905 -2081
- package/src/api/config.test.ts +47 -0
- package/src/api/config.ts +15 -0
- package/src/api/diagnose.ts +15 -15
- package/src/components/ConnectionErrorView.test.tsx +88 -0
- package/src/components/ConnectionErrorView.tsx +128 -22
- package/src/components/capacity/CapacityActivity.tsx +787 -0
- package/src/components/capacity/CapacityDemand.tsx +961 -0
- package/src/components/capacity/CapacityOverview.tsx +1529 -0
- package/src/components/capacity/CapacityPoolDetail.tsx +1626 -0
- package/src/components/capacity/CapacityView.test.tsx +2287 -0
- package/src/components/capacity/CapacityView.tsx +85 -0
- package/src/components/capacity/ClusterSchedulingCard.tsx +603 -0
- package/src/components/capacity/DemandNomination.test.tsx +151 -0
- package/src/components/capacity/certaintyGlyph.test.tsx +191 -0
- package/src/components/capacity/coverageCertainty.test.ts +162 -0
- package/src/components/capacity/podDemandGate.test.ts +47 -0
- package/src/components/capacity/podDemandGate.ts +22 -0
- package/src/components/capacity/schedulingBar.test.ts +244 -0
- package/src/components/capacity/shared.tsx +1841 -0
- package/src/components/diagnose/AISettings.tsx +21 -7
- package/src/components/diagnose/AgentSetupNotice.tsx +117 -0
- package/src/components/diagnose/DiagnoseContext.tsx +127 -57
- package/src/components/diagnose/DiagnoseSurface.tsx +33 -15
- package/src/components/diagnose/LocalDiagnoseAction.tsx +50 -27
- package/src/components/diagnose/agentCatalog.ts +30 -0
- package/src/components/diagnose/parts.test.tsx +125 -0
- package/src/components/diagnose/parts.tsx +166 -75
- package/src/components/home/CapacityCard.test.tsx +150 -0
- package/src/components/home/CapacityCard.tsx +125 -0
- package/src/components/home/HomeView.tsx +15 -1
- package/src/components/issues/IssuesPane.test.ts +142 -0
- package/src/components/issues/IssuesPane.tsx +142 -38
- package/src/components/nav/PrimaryNavRail.test.tsx +20 -0
- package/src/components/nav/PrimaryNavRail.tsx +191 -103
- package/src/components/resources/ResourcesView.tsx +9 -8
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +29 -1
- package/src/components/resources/renderers/PodRenderer.tsx +32 -3
- package/src/components/settings/SettingsDialog.tsx +31 -19
- package/src/components/timeline/TimelineView.tsx +17 -3
- package/src/components/ui/command-items.ts +222 -98
- package/src/components/workload/WorkloadView.tsx +16 -83
- package/src/context/ConnectionContext.test.ts +39 -0
- package/src/context/ConnectionContext.tsx +155 -51
- package/src/context/DiagnoseCustomization.tsx +1 -1
- package/src/utils/shell-safe.test.ts +55 -0
- package/src/utils/shell-safe.ts +21 -0
|
@@ -5,12 +5,16 @@
|
|
|
5
5
|
// Settings tabs' layout — this renders only the controls (no card, no heading).
|
|
6
6
|
import { useState } from "react";
|
|
7
7
|
import { Trash2 } from "lucide-react";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
clearHistory,
|
|
10
|
+
type AgentInfo,
|
|
11
|
+
type ExecutionProfile,
|
|
12
|
+
} from "../../api/diagnose";
|
|
9
13
|
import { AgentControls } from "./parts";
|
|
10
14
|
|
|
11
15
|
export interface AIDraft {
|
|
12
16
|
agent: string;
|
|
13
|
-
|
|
17
|
+
profile: ExecutionProfile;
|
|
14
18
|
model: string;
|
|
15
19
|
effort: string;
|
|
16
20
|
}
|
|
@@ -120,17 +124,27 @@ export function AISettingsSection({
|
|
|
120
124
|
// The agent, its model, and how it runs are all fixed by the host — none
|
|
121
125
|
// of the local BYO-agent knobs apply, so there's nothing to configure.
|
|
122
126
|
<p className="text-xs leading-snug text-theme-text-tertiary">
|
|
123
|
-
{agentLabel} manages the model and how it runs — there's nothing
|
|
124
|
-
configure here.
|
|
127
|
+
{agentLabel} manages the model and how it runs — there's nothing
|
|
128
|
+
to configure here.
|
|
125
129
|
</p>
|
|
126
130
|
) : (
|
|
127
131
|
<AgentControls
|
|
128
132
|
agents={agents}
|
|
129
133
|
selectedAgent={draft.agent}
|
|
130
134
|
// Model + effort are agent-specific; reset them when the agent changes.
|
|
131
|
-
onSelectAgent={(a) =>
|
|
132
|
-
|
|
133
|
-
|
|
135
|
+
onSelectAgent={(a) => {
|
|
136
|
+
const nextProfile = agents.find(
|
|
137
|
+
(agent) => agent.name === a,
|
|
138
|
+
)?.profiles?.[0];
|
|
139
|
+
onChange({
|
|
140
|
+
agent: a,
|
|
141
|
+
profile: nextProfile ?? draft.profile,
|
|
142
|
+
model: "",
|
|
143
|
+
effort: "",
|
|
144
|
+
});
|
|
145
|
+
}}
|
|
146
|
+
profile={draft.profile}
|
|
147
|
+
onSetProfile={(v) => onChange({ profile: v })}
|
|
134
148
|
model={draft.model}
|
|
135
149
|
onSetModel={(v) => onChange({ model: v })}
|
|
136
150
|
effort={draft.effort}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { Sparkles, Copy, Check, ExternalLink, RotateCw } from "lucide-react";
|
|
3
|
+
import { SUPPORTED_AGENTS, type AgentInstall } from "./agentCatalog";
|
|
4
|
+
import { type DiagnoseSetup } from "./DiagnoseContext";
|
|
5
|
+
|
|
6
|
+
function CopyButton({ text }: { text: string }) {
|
|
7
|
+
const [copied, setCopied] = useState(false);
|
|
8
|
+
return (
|
|
9
|
+
<button
|
|
10
|
+
type="button"
|
|
11
|
+
onClick={() => {
|
|
12
|
+
void navigator.clipboard?.writeText(text);
|
|
13
|
+
setCopied(true);
|
|
14
|
+
setTimeout(() => setCopied(false), 1100);
|
|
15
|
+
}}
|
|
16
|
+
className="shrink-0 rounded-md p-1 text-theme-text-tertiary hover:bg-theme-hover hover:text-theme-text-primary"
|
|
17
|
+
aria-label={copied ? "Copied" : "Copy install command"}
|
|
18
|
+
>
|
|
19
|
+
{copied ? (
|
|
20
|
+
<Check className="h-3.5 w-3.5 text-emerald-500" />
|
|
21
|
+
) : (
|
|
22
|
+
<Copy className="h-3.5 w-3.5" />
|
|
23
|
+
)}
|
|
24
|
+
</button>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function AgentRow({ agent }: { agent: AgentInstall }) {
|
|
29
|
+
return (
|
|
30
|
+
<div className="rounded-lg border border-theme-border bg-theme-base p-3">
|
|
31
|
+
<div className="mb-2 flex items-center justify-between gap-2">
|
|
32
|
+
<span className="text-sm font-medium text-theme-text-primary">
|
|
33
|
+
{agent.label}
|
|
34
|
+
</span>
|
|
35
|
+
<a
|
|
36
|
+
href={agent.docs}
|
|
37
|
+
target="_blank"
|
|
38
|
+
rel="noreferrer"
|
|
39
|
+
className="inline-flex items-center gap-1 text-xs text-theme-text-tertiary hover:text-theme-text-primary"
|
|
40
|
+
>
|
|
41
|
+
Docs
|
|
42
|
+
<ExternalLink className="h-3 w-3" />
|
|
43
|
+
</a>
|
|
44
|
+
</div>
|
|
45
|
+
<div className="flex items-center gap-1.5 rounded-md bg-theme-elevated px-2 py-1.5">
|
|
46
|
+
<code className="min-w-0 flex-1 overflow-x-auto whitespace-nowrap font-mono text-xs text-theme-text-secondary">
|
|
47
|
+
{agent.install}
|
|
48
|
+
</code>
|
|
49
|
+
<CopyButton text={agent.install} />
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Shown in the AI surface's Home when diagnosis is eligible in this deployment
|
|
56
|
+
// but not runnable yet — either no agent CLI is installed ("needs-install") or a
|
|
57
|
+
// supported one appeared after Radar booted ("needs-restart"). Radar decides the
|
|
58
|
+
// engine once at startup, so a fresh install needs a restart to take effect.
|
|
59
|
+
export function AgentSetupNotice({
|
|
60
|
+
setupState,
|
|
61
|
+
}: {
|
|
62
|
+
setupState: DiagnoseSetup;
|
|
63
|
+
}) {
|
|
64
|
+
const needsRestart = setupState === "needs-restart";
|
|
65
|
+
return (
|
|
66
|
+
<div className="mx-auto max-w-md px-1 py-6">
|
|
67
|
+
<div className="mb-3 flex h-10 w-10 items-center justify-center rounded-xl border border-accent/30 bg-accent/5">
|
|
68
|
+
<Sparkles className="h-5 w-5 text-accent" />
|
|
69
|
+
</div>
|
|
70
|
+
<h3 className="text-base font-semibold text-theme-text-primary">
|
|
71
|
+
{needsRestart
|
|
72
|
+
? "Restart Radar to enable AI diagnosis"
|
|
73
|
+
: "Set up AI diagnosis"}
|
|
74
|
+
</h3>
|
|
75
|
+
<p className="mt-1 text-sm text-theme-text-secondary">
|
|
76
|
+
{needsRestart ? (
|
|
77
|
+
<>
|
|
78
|
+
A supported agent CLI is now installed, but Radar started before it
|
|
79
|
+
was — restart Radar to pick it up. Investigations then run locally
|
|
80
|
+
on your machine, keyless, using your own agent.
|
|
81
|
+
</>
|
|
82
|
+
) : (
|
|
83
|
+
<>
|
|
84
|
+
Radar runs investigations through a coding agent CLI on your own
|
|
85
|
+
machine — no Radar cloud, no API key. Install one of these, then
|
|
86
|
+
restart Radar:
|
|
87
|
+
</>
|
|
88
|
+
)}
|
|
89
|
+
</p>
|
|
90
|
+
|
|
91
|
+
{!needsRestart && (
|
|
92
|
+
<div className="mt-4 flex flex-col gap-2.5">
|
|
93
|
+
{SUPPORTED_AGENTS.map((a) => (
|
|
94
|
+
<AgentRow key={a.name} agent={a} />
|
|
95
|
+
))}
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
|
|
99
|
+
{/* The panel only re-reads /api/agents on load, so after installing or
|
|
100
|
+
restarting the user needs a way to refresh in place rather than
|
|
101
|
+
hunting for a manual browser reload. */}
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
onClick={() => window.location.reload()}
|
|
105
|
+
className="btn-brand mt-4 inline-flex items-center gap-1.5 px-3 py-1.5 text-sm"
|
|
106
|
+
>
|
|
107
|
+
<RotateCw className="h-3.5 w-3.5" />
|
|
108
|
+
{needsRestart ? "Reload after restarting" : "Reload after installing"}
|
|
109
|
+
</button>
|
|
110
|
+
|
|
111
|
+
<p className="mt-4 text-xs text-theme-text-tertiary">
|
|
112
|
+
The agent reads this cluster through Radar and finds the root cause. It
|
|
113
|
+
never leaves your machine.
|
|
114
|
+
</p>
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
@@ -22,7 +22,11 @@ import {
|
|
|
22
22
|
recordConsent,
|
|
23
23
|
DiagnoseError,
|
|
24
24
|
} from "../../api/diagnose";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
type RunSummary,
|
|
27
|
+
type AgentInfo,
|
|
28
|
+
type ExecutionProfile,
|
|
29
|
+
} from "../../api/diagnose";
|
|
26
30
|
|
|
27
31
|
export interface Target {
|
|
28
32
|
kind: string;
|
|
@@ -31,15 +35,25 @@ export interface Target {
|
|
|
31
35
|
}
|
|
32
36
|
export type DiagnoseView = "home" | "investigation";
|
|
33
37
|
|
|
38
|
+
// Setup readiness of the local AI-diagnosis feature, derived from the agents API:
|
|
39
|
+
// - "ready": an agent is installed and the engine is running (available)
|
|
40
|
+
// - "needs-install": the feature is supported here but no agent CLI is installed
|
|
41
|
+
// - "needs-restart": a supported agent is now on PATH but Radar booted before it
|
|
42
|
+
// existed (the engine is decided once, at startup)
|
|
43
|
+
// - "off": not available in this deployment (proxy/OIDC auth, --no-mcp,
|
|
44
|
+
// or an embed host) — no install nudge would help
|
|
45
|
+
export type DiagnoseSetup = "ready" | "needs-install" | "needs-restart" | "off";
|
|
46
|
+
|
|
34
47
|
interface DiagnoseCtx {
|
|
35
48
|
available: boolean; // an agent CLI is present (button/entry gate)
|
|
49
|
+
setupState: DiagnoseSetup; // readiness for the setup nudge (see DiagnoseSetup)
|
|
36
50
|
agentLabel: string; // label of the selected agent, e.g. "Claude Code"
|
|
37
51
|
hosted: boolean; // selected agent runs on the host's backend, not this machine
|
|
38
52
|
agents: AgentInfo[]; // supported agents detected on PATH (for the picker)
|
|
39
53
|
selectedAgent: string; // name of the chosen backend ("claude"/"codex")
|
|
40
54
|
setSelectedAgent: (name: string) => void;
|
|
41
|
-
|
|
42
|
-
|
|
55
|
+
profile: ExecutionProfile;
|
|
56
|
+
setProfile: (v: ExecutionProfile) => void;
|
|
43
57
|
model: string; // optional model override ("" = the agent's own default)
|
|
44
58
|
setModel: (v: string) => void;
|
|
45
59
|
effort: string; // optional Codex reasoning effort ("" = default)
|
|
@@ -82,7 +96,11 @@ interface DiagnoseLayoutCtx {
|
|
|
82
96
|
|
|
83
97
|
// Stable key for "is THIS resource being investigated right now" — built the same way
|
|
84
98
|
// from a run summary and from a button's target so the two always match.
|
|
85
|
-
export function runTargetKey(
|
|
99
|
+
export function runTargetKey(
|
|
100
|
+
kind: string,
|
|
101
|
+
namespace: string,
|
|
102
|
+
name: string,
|
|
103
|
+
): string {
|
|
86
104
|
return `${kind} ${namespace} ${name}`;
|
|
87
105
|
}
|
|
88
106
|
|
|
@@ -91,7 +109,8 @@ const LayoutCtx = createContext<DiagnoseLayoutCtx | null>(null);
|
|
|
91
109
|
|
|
92
110
|
export function useDiagnoseLayout(): DiagnoseLayoutCtx {
|
|
93
111
|
const c = useContext(LayoutCtx);
|
|
94
|
-
if (!c)
|
|
112
|
+
if (!c)
|
|
113
|
+
throw new Error("useDiagnoseLayout must be used within DiagnoseProvider");
|
|
95
114
|
return c;
|
|
96
115
|
}
|
|
97
116
|
|
|
@@ -108,15 +127,10 @@ const WIDTH_KEY = "radar-ai-panel-width";
|
|
|
108
127
|
// Consent is machine-scoped and lives server-side (~/.radar): it gates a
|
|
109
128
|
// machine-scoped action (spawn this machine's agent CLI, persist transcripts to
|
|
110
129
|
// this machine's disk), so one acknowledgment covers this panel AND the
|
|
111
|
-
// `radar diagnose` CLI.
|
|
112
|
-
//
|
|
113
|
-
// approving Claude/Codex never bypasses Cursor's distinct disclosure.
|
|
114
|
-
type ConsentSurface = "standard" | "cursor";
|
|
115
|
-
function consentSurfaceFor(agent: string): ConsentSurface {
|
|
116
|
-
return agent === "cursor-agent" ? "cursor" : "standard";
|
|
117
|
-
}
|
|
130
|
+
// `radar diagnose` CLI. The execution profile selects the disclosure because
|
|
131
|
+
// it determines the actual access available to the agent process.
|
|
118
132
|
const AGENT_KEY = "radar-ai-agent";
|
|
119
|
-
const
|
|
133
|
+
const PROFILE_KEY = "radar-ai-profile";
|
|
120
134
|
const MODEL_KEY = "radar-ai-model";
|
|
121
135
|
const EFFORT_KEY = "radar-ai-effort";
|
|
122
136
|
// Push (reflow the app left) only while the app keeps at least this much width to
|
|
@@ -140,7 +154,9 @@ export function agentLabelFor(name: string, fallbackLabel?: string): string {
|
|
|
140
154
|
// openDiagnoseSettings opens the Settings dialog (App.tsx listens for this DOM
|
|
141
155
|
// event) — the canonical home for AI-diagnosis config.
|
|
142
156
|
export function openDiagnoseSettings() {
|
|
143
|
-
window.dispatchEvent(
|
|
157
|
+
window.dispatchEvent(
|
|
158
|
+
new CustomEvent("radar:open-settings", { detail: { section: "ai" } }),
|
|
159
|
+
);
|
|
144
160
|
}
|
|
145
161
|
|
|
146
162
|
function readStored(key: string): string | null {
|
|
@@ -160,15 +176,14 @@ function writeStored(key: string, value: string) {
|
|
|
160
176
|
|
|
161
177
|
export function DiagnoseProvider({ children }: { children: ReactNode }) {
|
|
162
178
|
const [available, setAvailable] = useState(false);
|
|
179
|
+
const [eligible, setEligible] = useState(false);
|
|
163
180
|
const [agents, setAgents] = useState<AgentInfo[]>([]);
|
|
164
|
-
const [consented, setConsented] = useState<
|
|
165
|
-
Record<ConsentSurface, boolean>
|
|
166
|
-
>({ standard: false, cursor: false });
|
|
181
|
+
const [consented, setConsented] = useState<Record<string, boolean>>({});
|
|
167
182
|
const [selectedAgent, setSelectedAgentState] = useState<string>(
|
|
168
183
|
() => readStored(AGENT_KEY) || "",
|
|
169
184
|
);
|
|
170
|
-
const [
|
|
171
|
-
() => readStored(
|
|
185
|
+
const [profile, setProfileState] = useState<ExecutionProfile>(
|
|
186
|
+
() => (readStored(PROFILE_KEY) as ExecutionProfile) || "safeguarded",
|
|
172
187
|
);
|
|
173
188
|
const [model, setModelState] = useState<string>(
|
|
174
189
|
() => readStored(MODEL_KEY) || "",
|
|
@@ -205,12 +220,16 @@ export function DiagnoseProvider({ children }: { children: ReactNode }) {
|
|
|
205
220
|
fetchAgents()
|
|
206
221
|
.then((r) => {
|
|
207
222
|
if (!live) return;
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
223
|
+
setConsented(r.consented ?? {});
|
|
224
|
+
setEligible(!!r.eligible);
|
|
225
|
+
const supported = r.agents.filter(
|
|
226
|
+
(a) =>
|
|
227
|
+
a.supported &&
|
|
228
|
+
(a.hosted ||
|
|
229
|
+
(!!a.profiles?.length &&
|
|
230
|
+
a.profiles.every((p) => !!a.consentSurfaces?.[p]))),
|
|
231
|
+
);
|
|
232
|
+
setAvailable(r.enabled && supported.length > 0);
|
|
214
233
|
setAgents(supported);
|
|
215
234
|
// Keep the stored pick only if it's still installed; else default to the
|
|
216
235
|
// first supported agent (matches the server's default selection).
|
|
@@ -220,6 +239,13 @@ export function DiagnoseProvider({ children }: { children: ReactNode }) {
|
|
|
220
239
|
? stored
|
|
221
240
|
: (supported[0]?.name ?? "");
|
|
222
241
|
setSelectedAgentState(next);
|
|
242
|
+
const profiles = supported.find((a) => a.name === next)?.profiles ?? [];
|
|
243
|
+
const storedProfile =
|
|
244
|
+
(readStored(PROFILE_KEY) as ExecutionProfile) || "safeguarded";
|
|
245
|
+
if (!profiles.includes(storedProfile) && profiles[0]) {
|
|
246
|
+
setProfileState(profiles[0]);
|
|
247
|
+
writeStored(PROFILE_KEY, profiles[0]);
|
|
248
|
+
}
|
|
223
249
|
// Model/effort are agent-specific; if the stored agent is gone, its values
|
|
224
250
|
// don't apply to the fallback agent (e.g. a Codex slug under Claude) — drop them.
|
|
225
251
|
if (next !== stored) {
|
|
@@ -247,23 +273,45 @@ export function DiagnoseProvider({ children }: { children: ReactNode }) {
|
|
|
247
273
|
(name: string) => {
|
|
248
274
|
setSelectedAgentState(name);
|
|
249
275
|
writeStored(AGENT_KEY, name);
|
|
276
|
+
const nextProfile = agents.find((agent) => agent.name === name)
|
|
277
|
+
?.profiles?.[0];
|
|
278
|
+
if (nextProfile) {
|
|
279
|
+
setProfileState(nextProfile);
|
|
280
|
+
writeStored(PROFILE_KEY, nextProfile);
|
|
281
|
+
}
|
|
250
282
|
// Model + effort are agent-specific (Claude aliases vs Codex slugs); reset
|
|
251
283
|
// to the new agent's default rather than carry an invalid value across.
|
|
252
284
|
setModel("");
|
|
253
285
|
setEffort("");
|
|
254
286
|
},
|
|
255
|
-
[setModel, setEffort],
|
|
287
|
+
[agents, setModel, setEffort],
|
|
256
288
|
);
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
writeStored(
|
|
289
|
+
const setProfile = useCallback((v: ExecutionProfile) => {
|
|
290
|
+
setProfileState(v);
|
|
291
|
+
writeStored(PROFILE_KEY, v);
|
|
260
292
|
}, []);
|
|
261
293
|
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const
|
|
294
|
+
const selectedAgentInfo = agents.find((a) => a.name === selectedAgent);
|
|
295
|
+
const effectiveProfile = selectedAgentInfo?.profiles?.includes(profile)
|
|
296
|
+
? profile
|
|
297
|
+
: (selectedAgentInfo?.profiles?.[0] ?? profile);
|
|
298
|
+
const agentLabel = agentLabelFor(selectedAgent, selectedAgentInfo?.label);
|
|
299
|
+
const hosted = !!selectedAgentInfo?.hosted;
|
|
300
|
+
// Hosted Radar uses its existing per-user disclosure surface and supplies its
|
|
301
|
+
// own copy. Local agents use the execution profile as the consent contract.
|
|
302
|
+
const consentSurface = hosted
|
|
303
|
+
? "standard"
|
|
304
|
+
: (selectedAgentInfo?.consentSurfaces?.[effectiveProfile] ?? "");
|
|
305
|
+
|
|
306
|
+
// `agents` holds only supported CLIs (filtered on fetch), so a non-empty list
|
|
307
|
+
// while the engine is off means a drivable agent appeared on PATH after boot.
|
|
308
|
+
const setupState: DiagnoseSetup = available
|
|
309
|
+
? "ready"
|
|
310
|
+
: !eligible
|
|
311
|
+
? "off"
|
|
312
|
+
: agents.length > 0
|
|
313
|
+
? "needs-restart"
|
|
314
|
+
: "needs-install";
|
|
267
315
|
|
|
268
316
|
useEffect(() => {
|
|
269
317
|
const onResize = () => setViewportW(window.innerWidth);
|
|
@@ -314,10 +362,6 @@ export function DiagnoseProvider({ children }: { children: ReactNode }) {
|
|
|
314
362
|
return () => clearInterval(t);
|
|
315
363
|
}, [open, available, hasRunning, refreshRuns]);
|
|
316
364
|
|
|
317
|
-
// Keep the live selected agent reachable from the [] -dep callbacks below
|
|
318
|
-
// (consent is agent-specific, so they must read the CURRENT pick, not a closure).
|
|
319
|
-
const selectedAgentRef = useRef(selectedAgent);
|
|
320
|
-
selectedAgentRef.current = selectedAgent;
|
|
321
365
|
const consentedRef = useRef(consented);
|
|
322
366
|
consentedRef.current = consented;
|
|
323
367
|
|
|
@@ -329,7 +373,7 @@ export function DiagnoseProvider({ children }: { children: ReactNode }) {
|
|
|
329
373
|
const seq = ++startSeqRef.current;
|
|
330
374
|
createRun(t, {
|
|
331
375
|
agent: selectedAgent || undefined,
|
|
332
|
-
|
|
376
|
+
profile: hosted ? undefined : effectiveProfile,
|
|
333
377
|
model: model || undefined,
|
|
334
378
|
effort: effort || undefined,
|
|
335
379
|
})
|
|
@@ -351,17 +395,28 @@ export function DiagnoseProvider({ children }: { children: ReactNode }) {
|
|
|
351
395
|
});
|
|
352
396
|
};
|
|
353
397
|
|
|
354
|
-
const openInvestigation = useCallback(
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
398
|
+
const openInvestigation = useCallback(
|
|
399
|
+
(t: Target) => {
|
|
400
|
+
setStartError(null);
|
|
401
|
+
setOpen(true);
|
|
402
|
+
if (!hosted && !consentSurface) {
|
|
403
|
+
setPendingTarget(null);
|
|
404
|
+
setStartError(
|
|
405
|
+
"Radar can’t run this agent with a verified execution profile.",
|
|
406
|
+
);
|
|
407
|
+
setView("investigation");
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (!consentedRef.current[consentSurface]) {
|
|
411
|
+
setPendingTarget(t);
|
|
412
|
+
setView("investigation");
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
359
415
|
setView("investigation");
|
|
360
|
-
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
}, []);
|
|
416
|
+
startRunRef.current(t);
|
|
417
|
+
},
|
|
418
|
+
[consentSurface, hosted],
|
|
419
|
+
);
|
|
365
420
|
const openRun = useCallback((id: string) => {
|
|
366
421
|
setStartError(null);
|
|
367
422
|
setActiveRunId(id);
|
|
@@ -384,7 +439,9 @@ export function DiagnoseProvider({ children }: { children: ReactNode }) {
|
|
|
384
439
|
window.history.replaceState(
|
|
385
440
|
null,
|
|
386
441
|
"",
|
|
387
|
-
window.location.pathname +
|
|
442
|
+
window.location.pathname +
|
|
443
|
+
(qs ? `?${qs}` : "") +
|
|
444
|
+
window.location.hash,
|
|
388
445
|
);
|
|
389
446
|
}
|
|
390
447
|
} catch {
|
|
@@ -401,15 +458,18 @@ export function DiagnoseProvider({ children }: { children: ReactNode }) {
|
|
|
401
458
|
const consentBusyRef = useRef(false);
|
|
402
459
|
const approveConsent = useCallback(() => {
|
|
403
460
|
if (consentBusyRef.current) return;
|
|
461
|
+
if (!consentSurface) {
|
|
462
|
+
setStartError("Radar can’t record consent for this agent.");
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
404
465
|
consentBusyRef.current = true;
|
|
405
466
|
setStartError(null);
|
|
406
|
-
const surface = consentSurfaceFor(selectedAgentRef.current);
|
|
407
467
|
const t = pendingTarget;
|
|
408
468
|
// The server ENFORCES consent at start, so the acknowledgment must land
|
|
409
469
|
// before the run request — awaiting also makes it durable for the CLI.
|
|
410
|
-
recordConsent(
|
|
470
|
+
recordConsent(consentSurface)
|
|
411
471
|
.then(() => {
|
|
412
|
-
setConsented((prev) => ({ ...prev, [
|
|
472
|
+
setConsented((prev) => ({ ...prev, [consentSurface]: true }));
|
|
413
473
|
// Cleared only on success: a failed write keeps the consent card up
|
|
414
474
|
// (needsConsent = !!pendingTarget) so "try again" works in place.
|
|
415
475
|
setPendingTarget(null);
|
|
@@ -421,7 +481,7 @@ export function DiagnoseProvider({ children }: { children: ReactNode }) {
|
|
|
421
481
|
.finally(() => {
|
|
422
482
|
consentBusyRef.current = false;
|
|
423
483
|
});
|
|
424
|
-
}, [pendingTarget]);
|
|
484
|
+
}, [consentSurface, pendingTarget]);
|
|
425
485
|
const cancelConsent = useCallback(() => {
|
|
426
486
|
setPendingTarget(null);
|
|
427
487
|
setOpen(false);
|
|
@@ -435,13 +495,14 @@ export function DiagnoseProvider({ children }: { children: ReactNode }) {
|
|
|
435
495
|
|
|
436
496
|
const value: DiagnoseCtx = {
|
|
437
497
|
available,
|
|
498
|
+
setupState,
|
|
438
499
|
agentLabel,
|
|
439
500
|
hosted,
|
|
440
501
|
agents,
|
|
441
502
|
selectedAgent,
|
|
442
503
|
setSelectedAgent,
|
|
443
|
-
|
|
444
|
-
|
|
504
|
+
profile: effectiveProfile,
|
|
505
|
+
setProfile,
|
|
445
506
|
model,
|
|
446
507
|
setModel,
|
|
447
508
|
effort,
|
|
@@ -484,7 +545,16 @@ export function DiagnoseProvider({ children }: { children: ReactNode }) {
|
|
|
484
545
|
panelWidthKey: WIDTH_KEY,
|
|
485
546
|
runningKeys,
|
|
486
547
|
}),
|
|
487
|
-
[
|
|
548
|
+
[
|
|
549
|
+
open,
|
|
550
|
+
contentGutter,
|
|
551
|
+
maximized,
|
|
552
|
+
width,
|
|
553
|
+
narrow,
|
|
554
|
+
runningKeys,
|
|
555
|
+
setMaximized,
|
|
556
|
+
setWidth,
|
|
557
|
+
],
|
|
488
558
|
);
|
|
489
559
|
|
|
490
560
|
return (
|
|
@@ -26,27 +26,32 @@ import {
|
|
|
26
26
|
import { useDiagnoseCustomization } from "../../context/DiagnoseCustomization";
|
|
27
27
|
import { InvestigationView } from "./InvestigationView";
|
|
28
28
|
import { RecentList } from "./Home";
|
|
29
|
+
import { AgentSetupNotice } from "./AgentSetupNotice";
|
|
29
30
|
import { ConsentCard } from "./parts";
|
|
30
31
|
import { buildLaunchCommand, launchAgentLabel, openInTerminal } from "./launch";
|
|
31
|
-
import { type RunSummary } from "../../api/diagnose";
|
|
32
|
+
import { type RunSummary, type ExecutionProfile } from "../../api/diagnose";
|
|
32
33
|
|
|
33
34
|
function capWord(s: string): string {
|
|
34
35
|
return s ? s[0].toUpperCase() + s.slice(1) : s;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
// buildConfigLine renders the active AI config as the header subtitle. Codex shows
|
|
38
|
-
// its
|
|
39
|
+
// its execution profile + effective reasoning effort (Default → medium); a model
|
|
39
40
|
// override is shown for either agent. Reflects a run's recorded settings, or the
|
|
40
41
|
// current defaults on Home.
|
|
41
42
|
function buildConfigLine(cfg: {
|
|
42
43
|
agent?: string;
|
|
43
|
-
|
|
44
|
+
profile?: ExecutionProfile;
|
|
44
45
|
model?: string;
|
|
45
46
|
effort?: string;
|
|
46
47
|
}): string {
|
|
47
48
|
const parts = [agentLabelFor(cfg.agent ?? "")];
|
|
49
|
+
if (cfg.profile) {
|
|
50
|
+
parts.push(
|
|
51
|
+
cfg.profile === "full-local" ? "Your agent setup" : "Radar safeguards",
|
|
52
|
+
);
|
|
53
|
+
}
|
|
48
54
|
if (cfg.agent === "codex") {
|
|
49
|
-
parts.push(cfg.isolated === false ? "My setup" : "Isolated");
|
|
50
55
|
parts.push(`${capWord(cfg.effort || "medium")} effort`);
|
|
51
56
|
}
|
|
52
57
|
if (cfg.model) parts.push(capWord(cfg.model));
|
|
@@ -139,7 +144,8 @@ export function DiagnoseSurface({ topInset = 0 }: { topInset?: number }) {
|
|
|
139
144
|
const d = useDiagnose();
|
|
140
145
|
// Injected settings action: undefined = Radar's own Settings dialog;
|
|
141
146
|
// null = hide the gear + links.
|
|
142
|
-
const { consentCopy, onOpenSettings: hostOpenSettings } =
|
|
147
|
+
const { consentCopy, onOpenSettings: hostOpenSettings } =
|
|
148
|
+
useDiagnoseCustomization();
|
|
143
149
|
const openSettings =
|
|
144
150
|
hostOpenSettings === undefined ? openDiagnoseSettings : hostOpenSettings;
|
|
145
151
|
const {
|
|
@@ -174,18 +180,23 @@ export function DiagnoseSurface({ topInset = 0 }: { topInset?: number }) {
|
|
|
174
180
|
document.addEventListener("mouseup", onUp);
|
|
175
181
|
};
|
|
176
182
|
|
|
183
|
+
// Feature is eligible here but not runnable yet (no agent installed, or one
|
|
184
|
+
// appeared after boot) — Home leads with the setup notice instead of an empty list.
|
|
185
|
+
const setupPending =
|
|
186
|
+
d.setupState === "needs-install" || d.setupState === "needs-restart";
|
|
187
|
+
|
|
177
188
|
const activeRun = d.runs.find((r) => r.id === d.activeRunId) ?? null;
|
|
178
189
|
// A focused run shows the agent it actually ran with; Home reflects the current pick.
|
|
179
190
|
const activeAgentLabel = activeRun?.agent
|
|
180
191
|
? agentLabelFor(activeRun.agent)
|
|
181
192
|
: d.agentLabel;
|
|
182
193
|
// Header subtitle: the config a focused run actually used (it records agent /
|
|
183
|
-
//
|
|
194
|
+
// profile / model / effort), or the current defaults on Home. Codex shows mode
|
|
184
195
|
// + reasoning effort; model is shown only when overridden. Clicking opens Settings.
|
|
185
196
|
const configLine = buildConfigLine(
|
|
186
197
|
activeRun ?? {
|
|
187
198
|
agent: d.selectedAgent,
|
|
188
|
-
|
|
199
|
+
profile: d.hosted ? undefined : d.profile,
|
|
189
200
|
model: d.model,
|
|
190
201
|
effort: d.effort,
|
|
191
202
|
},
|
|
@@ -208,7 +219,7 @@ export function DiagnoseSurface({ topInset = 0 }: { topInset?: number }) {
|
|
|
208
219
|
<ConsentCard
|
|
209
220
|
agentName={d.agentLabel}
|
|
210
221
|
agent={d.selectedAgent}
|
|
211
|
-
|
|
222
|
+
profile={d.profile}
|
|
212
223
|
copy={consentCopy}
|
|
213
224
|
onOpenSettings={openSettings ?? undefined}
|
|
214
225
|
onApprove={d.approveConsent}
|
|
@@ -258,6 +269,10 @@ export function DiagnoseSurface({ topInset = 0 }: { topInset?: number }) {
|
|
|
258
269
|
Dismiss
|
|
259
270
|
</button>
|
|
260
271
|
</div>
|
|
272
|
+
) : setupPending ? (
|
|
273
|
+
<div className="flex-1 overflow-y-auto">
|
|
274
|
+
<AgentSetupNotice setupState={d.setupState} />
|
|
275
|
+
</div>
|
|
261
276
|
) : (
|
|
262
277
|
<div className="flex flex-1 items-center justify-center px-6 text-center text-sm text-theme-text-tertiary">
|
|
263
278
|
Select an investigation, or open a resource and click Diagnose.
|
|
@@ -357,7 +372,7 @@ export function DiagnoseSurface({ topInset = 0 }: { topInset?: number }) {
|
|
|
357
372
|
only appears when expanded; keys keep the detail node identity-stable
|
|
358
373
|
as it comes and goes. */}
|
|
359
374
|
<div className="flex min-h-0 flex-1">
|
|
360
|
-
{showHistory && (
|
|
375
|
+
{showHistory && (!setupPending || d.runs.length > 0) && (
|
|
361
376
|
<aside
|
|
362
377
|
key="recent"
|
|
363
378
|
className="w-72 shrink-0 overflow-y-auto border-r border-theme-border px-3 py-3"
|
|
@@ -376,12 +391,15 @@ export function DiagnoseSurface({ topInset = 0 }: { topInset?: number }) {
|
|
|
376
391
|
key="main"
|
|
377
392
|
className="flex-1 overflow-y-auto overflow-x-hidden px-4 py-3"
|
|
378
393
|
>
|
|
379
|
-
<
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
394
|
+
{setupPending && <AgentSetupNotice setupState={d.setupState} />}
|
|
395
|
+
{(!setupPending || d.runs.length > 0) && (
|
|
396
|
+
<RecentList
|
|
397
|
+
agentLabel={d.agentLabel}
|
|
398
|
+
runs={d.runs}
|
|
399
|
+
onSelect={d.openRun}
|
|
400
|
+
historyDegraded={d.historyDegraded}
|
|
401
|
+
/>
|
|
402
|
+
)}
|
|
385
403
|
</div>
|
|
386
404
|
) : (
|
|
387
405
|
<div key="main" className="flex min-h-0 min-w-0 flex-1 flex-col">
|