diff --git a/src/components/ops/doc-intelligence-drawer.tsx b/src/components/ops/doc-intelligence-drawer.tsx
index 71923c4..0928aa2 100644
--- a/src/components/ops/doc-intelligence-drawer.tsx
+++ b/src/components/ops/doc-intelligence-drawer.tsx
@@ -95,11 +95,25 @@ export function DocIntelligenceBody({ documentId }: { documentId: string }) {
icon={}
title="No entities found"
hint={
- <>
- No entities are stored for this document. Extraction may not have run yet (enable
- it with KB_INTELLIGENCE_ENABLED), or the document yielded none. Try
- Re-extract.
- >
+ intel.data?.capability && !intel.data.capability.intelligence_enabled ? (
+ <>
+ Intelligence extraction is disabled — enable it with{" "}
+ KB_INTELLIGENCE_ENABLED, or use Re-extract, which works
+ while disabled.
+ >
+ ) : intel.data?.capability &&
+ intel.data.capability.credential_configured === false ? (
+ <>
+ Extraction is enabled but no API key resolves for the extraction endpoint, so
+ extraction fails silently. Add a key under Knowledge Base settings (or set{" "}
+ OPENROUTER_API_KEY), then Re-extract.
+ >
+ ) : (
+ <>
+ No entities are stored for this document — it may genuinely yield none. Try{" "}
+ Re-extract.
+ >
+ )
}
/>
) : (
diff --git a/src/components/ops/graph-lens-helpers.test.ts b/src/components/ops/graph-lens-helpers.test.ts
index 06bfec4..2f180a5 100644
--- a/src/components/ops/graph-lens-helpers.test.ts
+++ b/src/components/ops/graph-lens-helpers.test.ts
@@ -1,43 +1,49 @@
-import { describe, it, expect } from "vitest";
-import { deriveGraphState, isSmallModel, fromIntelligence } from "./graph-lens-helpers";
-import type { KbCapability, KbDocumentIntelligence } from "@/lib/types";
+import { describe, expect, it } from "vitest";
-const on: KbCapability = { intelligence_enabled: true, extraction_model: "x" };
-const off: KbCapability = { intelligence_enabled: false, extraction_model: "x" };
+import type { KbCapability } from "@/lib/types";
-describe("deriveGraphState", () => {
- it("loading before first data", () =>
- expect(deriveGraphState(undefined, undefined, true, false)).toBe("loading"));
- it("disabled when capability is off", () =>
- expect(deriveGraphState(off, 0, false, true)).toBe("disabled"));
- it("empty when enabled with 0 entities", () =>
- expect(deriveGraphState(on, 0, false, true)).toBe("empty"));
- it("ready when entities present", () =>
- expect(deriveGraphState(on, 5, false, true)).toBe("ready"));
+import { deriveGraphState } from "./graph-lens-helpers";
+
+// Plan 097 (RantAIClaw): the console's honesty about WHY a graph is empty
+// lives entirely in this pure function — these are the only tests pinning it.
+
+const cap = (over: Partial = {}): KbCapability => ({
+ intelligence_enabled: true,
+ extraction_model: "openai/gpt-4.1-nano",
+ credential_configured: true,
+ ...over,
});
-describe("isSmallModel", () => {
- it("flags nano/mini, not large models", () => {
- expect(isSmallModel("openai/gpt-4.1-nano")).toBe(true);
- expect(isSmallModel("openai/gpt-4o")).toBe(false);
- expect(isSmallModel(undefined)).toBe(false);
+describe("deriveGraphState", () => {
+ it("loading before the first response", () => {
+ expect(deriveGraphState(undefined, undefined, true, false)).toBe("loading");
+ });
+
+ it("disabled when intelligence is off", () => {
+ expect(deriveGraphState(cap({ intelligence_enabled: false }), 0, false, true)).toBe(
+ "disabled",
+ );
+ });
+
+ it("no-credential when enabled but no key resolves", () => {
+ expect(deriveGraphState(cap({ credential_configured: false }), 0, false, true)).toBe(
+ "no-credential",
+ );
+ });
+
+ it("empty when enabled with a key and zero entities", () => {
+ expect(deriveGraphState(cap(), 0, false, true)).toBe("empty");
+ });
+
+ it("ready when entities exist", () => {
+ expect(deriveGraphState(cap(), 12, false, true)).toBe("ready");
});
-});
-describe("fromIntelligence", () => {
- it("builds nodes/edges and computes degree from relations", () => {
- const intel: KbDocumentIntelligence = {
- entities: [
- { id: "a", name: "A", entity_type: "person", confidence: 1 },
- { id: "b", name: "B", entity_type: "person", confidence: 1 },
- ],
- relations: [{ id: "r", source: "a", target: "b", relation_type: "knows", confidence: 1 }],
- stats: {},
- };
- const g = fromIntelligence(intel);
- expect(g.nodes.length).toBe(2);
- expect(g.edges.length).toBe(1);
- expect(g.nodes.find((n) => n.id === "a")!.degree).toBe(1);
- expect(g.stats?.corpus_entities).toBe(2);
+ it("older gateways without the credential field fall through to empty, never no-credential", () => {
+ // credential_configured is optional in the wire type; undefined must not
+ // be treated as "missing credential".
+ expect(deriveGraphState(cap({ credential_configured: undefined }), 0, false, true)).toBe(
+ "empty",
+ );
});
});
diff --git a/src/components/ops/graph-lens-helpers.ts b/src/components/ops/graph-lens-helpers.ts
index 4f9ddb1..31fba4e 100644
--- a/src/components/ops/graph-lens-helpers.ts
+++ b/src/components/ops/graph-lens-helpers.ts
@@ -6,7 +6,7 @@ import type {
KbGraphNode,
} from "@/lib/types";
-export type GraphState = "loading" | "disabled" | "empty" | "ready";
+export type GraphState = "loading" | "disabled" | "no-credential" | "empty" | "ready";
/**
* Derive the honest render state from the capability signal + scope-wide entity
@@ -20,6 +20,11 @@ export function deriveGraphState(
): GraphState {
if (loading && !hasData) return "loading";
if (cap && !cap.intelligence_enabled) return "disabled";
+ // Enabled but no key resolves: extraction fails per chunk and is
+ // swallowed server-side, so without this distinction the operator sees
+ // the same "empty" state as a genuinely entity-free corpus.
+ if (cap && cap.intelligence_enabled && cap.credential_configured === false)
+ return "no-credential";
return (corpusEntities ?? 0) === 0 ? "empty" : "ready";
}
diff --git a/src/components/ops/graph-lens.tsx b/src/components/ops/graph-lens.tsx
index 4aabfaf..0c6f02f 100644
--- a/src/components/ops/graph-lens.tsx
+++ b/src/components/ops/graph-lens.tsx
@@ -159,6 +159,21 @@ export function GraphLens({ scope, lockScope }: { scope: GraphScope; lockScope?:
>
}
/>
+ ) : graphState === "no-credential" ? (
+ }
+ title="Extraction is on, but no credential resolves"
+ hint={
+ <>
+ Intelligence extraction is enabled but no API key resolves for the extraction
+ endpoint, so every extraction attempt fails silently. Add a key under Knowledge
+ Base settings (or set OPENROUTER_API_KEY).
+ {data?.capability?.extraction_model && (
+
+ )}
+ >
+ }
+ />
) : graphState === "empty" ? (
}
diff --git a/src/lib/types.ts b/src/lib/types.ts
index 601e3b9..3697a0a 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -232,6 +232,10 @@ export interface KbGraphEdge {
export interface KbCapability {
intelligence_enabled: boolean;
extraction_model: string;
+ /** Presence-only credential signal — the key itself never crosses the API. */
+ credential_configured?: boolean;
+ graphrag_enabled?: boolean;
+ resolution?: string;
}
export interface KbGraph {
nodes: KbGraphNode[];