@frankieai/react 0.2.7 → 0.2.8

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frankieai/react",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "Embeddable, themeable chat widget for the Frankie onboarding agent — shadcn/ui components, works with or without Tailwind in the host app.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -6,7 +6,7 @@ import { useFrankieChat } from "../../hooks/useFrankieChat.js";
6
6
  /** Reports server-confirmed skills for the current turn; never implies drafts are active. */
7
7
  export function AgentContext() {
8
8
  const { loading, configError } = useFrankie();
9
- const { loadedSkills, busy, restoring, error } = useFrankieChat();
9
+ const { loadedSkills, activeApis, busy, restoring, error } = useFrankieChat();
10
10
  const status = configError || error ? "Unavailable" : loading || restoring ? "Connecting" : busy && loadedSkills === null ? "Checking" : loadedSkills?.length ? "Active" : loadedSkills ? "Tools only" : "Ready";
11
11
  return <div className="frankie-agent-context-wrap">
12
12
  <details className="frankie-agent-context">
@@ -15,6 +15,7 @@ export function AgentContext() {
15
15
  <strong>{loadedSkills === null ? "Skills are checked when you send a message" : loadedSkills.length ? `${loadedSkills.length} ${loadedSkills.length === 1 ? "skill" : "skills"} loaded` : "No skills loaded for this turn"}</strong>
16
16
  {loadedSkills?.length ? <ul>{loadedSkills.map(skill => <li key={skill.id}><span>{skill.name}</span><small>v{skill.revision}</small></li>)}</ul> : <p>Frankie can use the tools approved for this workspace. Drafts and skills without available tools are not loaded.</p>}
17
17
  <p>Published skills and updates are checked on every message. Only approved, available tools can run.</p>
18
+ {activeApis?.length ? <ul className="frankie-active-apis">{activeApis.map(api => <li key={api.name}><span>{api.name}</span><small>spec {api.specHash} · {api.operationCount} {api.operationCount === 1 ? "operation" : "operations"}</small></li>)}</ul> : null}
18
19
  </div>
19
20
  </details>
20
21
  <span className="frankie-sr-only" role="status">{loadedSkills === null ? status : `${loadedSkills.length} ${loadedSkills.length === 1 ? "skill" : "skills"} loaded. ${status}.`}</span>
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { createContext, useContext, useCallback, useEffect, useRef, useState } from "react";
4
- import type { ChatAttachment, LoadedSkill } from "@frankieai/core";
4
+ import type { ApiToolInventory, ChatAttachment, LoadedSkill } from "@frankieai/core";
5
5
  import { FrankieApiError } from "../lib/client.js";
6
6
  import {
7
7
  applyEvent,
@@ -20,6 +20,8 @@ export interface SendOptions {
20
20
 
21
21
  export interface UseFrankieChatResult {
22
22
  loadedSkills: LoadedSkill[] | null;
23
+ /** Which OpenAPI sources are live for this turn, and a content fingerprint of each. */
24
+ activeApis: ApiToolInventory[] | null;
23
25
  restoring: boolean;
24
26
  messages: ChatMessage[];
25
27
  /** True from the moment a message is sent until the turn ends. */
@@ -50,6 +52,7 @@ export function useFrankieChatState(): UseFrankieChatResult {
50
52
  const [error, setError] = useState<string | undefined>(undefined);
51
53
  const [restored, setRestored] = useState(false);
52
54
  const [loadedSkills, setLoadedSkills] = useState<LoadedSkill[] | null>(null);
55
+ const [activeApis, setActiveApis] = useState<ApiToolInventory[] | null>(null);
53
56
  const abortRef = useRef<AbortController | null>(null);
54
57
  const mounted = useRef(true);
55
58
  // State updates can be batched while a stream is arriving. Keep the latest
@@ -142,6 +145,7 @@ export function useFrankieChatState(): UseFrankieChatResult {
142
145
  const assistantId = nextId("a");
143
146
  setError(undefined);
144
147
  setLoadedSkills(null);
148
+ setActiveApis(null);
145
149
  setBusy(true);
146
150
  const next = [
147
151
  ...messagesRef.current,
@@ -171,6 +175,7 @@ export function useFrankieChatState(): UseFrankieChatResult {
171
175
  },
172
176
  (event) => {
173
177
  if (event.type === "skills_loaded") { setLoadedSkills(event.skills); return; }
178
+ if (event.type === "tools_active") { setActiveApis(event.apis); return; }
174
179
  if (event.type === "ping" || event.type === "session") return;
175
180
  if (event.type === "text_delta") receivedText = true;
176
181
  if (event.type === "tool_start" || event.type === "tool_end") receivedToolActivity = true;
@@ -236,6 +241,7 @@ export function useFrankieChatState(): UseFrankieChatResult {
236
241
  setBusy(false);
237
242
  setRestored(true);
238
243
  setLoadedSkills(null);
244
+ setActiveApis(null);
239
245
  try {
240
246
  if (client.persist) window.localStorage.removeItem(`${client.namespace}.messages`);
241
247
  } catch {
@@ -243,7 +249,7 @@ export function useFrankieChatState(): UseFrankieChatResult {
243
249
  }
244
250
  }, [client]);
245
251
 
246
- return { messages, busy, loadedSkills, restoring: !restored, ...(error ? { error } : {}), send, stop, reset };
252
+ return { messages, busy, loadedSkills, activeApis, restoring: !restored, ...(error ? { error } : {}), send, stop, reset };
247
253
  }
248
254
 
249
255
 
@@ -261,6 +261,23 @@ describe("ChatPanel", () => {
261
261
  await waitFor(() => expect(scrollTop).toBe(scroller.scrollHeight));
262
262
  cleanup();
263
263
  });
264
+ it("shows the active API spec hash and operation count once tools_active arrives", async () => {
265
+ cleanup();
266
+ const fetchImpl = vi.fn(async (url: string | URL | Request) =>
267
+ String(url).endsWith("/v1/session")
268
+ ? Response.json({ sessionId: "apis", sessionToken: "fixture", expiresAt: new Date(Date.now() + 600_000).toISOString(), user: { id: "one", anonymous: true } })
269
+ : new Response(sseStream([{ type: "tools_active", apis: [{ name: "acme", specHash: "abc123def456", operationCount: 4 }] }, { type: "text_delta", text: "Done" }, { type: "turn_end", stopReason: "end_turn" }])),
270
+ );
271
+ render(<FrankieProvider publishableKey="pk_apis_fixture" config={config} fetchImpl={fetchImpl} persist={false}><ChatPanel /></FrankieProvider>);
272
+ await waitFor(() => expect((screen.getByLabelText("Message") as HTMLTextAreaElement).disabled).toBe(false));
273
+ fireEvent.change(screen.getByLabelText("Message"), { target: { value: "Hi" } });
274
+ fireEvent.click(screen.getByRole("button", { name: "Send" }));
275
+ await screen.findByText("Done");
276
+ fireEvent.click(screen.getByLabelText("Agent context"));
277
+ expect(screen.getByText("acme")).toBeTruthy();
278
+ expect(screen.getByText("spec abc123def456 · 4 operations")).toBeTruthy();
279
+ });
280
+
264
281
  it("refreshes the context count and revisions from each server turn", async () => {
265
282
  cleanup();
266
283
  let turn = 0;