akanjs 3.0.0-alpha.30 → 3.0.0-alpha.31

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": "akanjs",
3
- "version": "3.0.0-alpha.30",
3
+ "version": "3.0.0-alpha.31",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -1,3 +1,4 @@
1
+ import { type ReactNode } from "react";
1
2
  import { type AgentRunner } from "../../vendor/use-agentic.d.ts";
2
3
  import { type PersistOption } from "./sessionHistory.d.ts";
3
4
  export interface ChatProps {
@@ -11,6 +12,8 @@ export interface ChatProps {
11
12
  defaultOpen?: boolean;
12
13
  /** Keeps the transcript across reloads — sessionStorage by default, `{ storage: "local" }` to outlive the tab. */
13
14
  persist?: PersistOption;
15
+ /** Renders in the page flow instead of floating above it — a zone chat that lives inside its own section. */
16
+ inline?: boolean;
14
17
  }
15
18
  /**
16
19
  * The user-facing half of the in-page agent: one floating chat wired to the same surface the dock inspects.
@@ -19,6 +22,6 @@ export interface ChatProps {
19
22
  * no history). An enclosing AgentProvider's session wins, which is how an app isolates a surface or swaps the
20
23
  * loop while keeping this UI.
21
24
  */
22
- export declare const DefaultChat: ({ className, title, instructions, runner, maxTurns, defaultOpen, persist, }: ChatProps) => import("react/jsx-runtime").JSX.Element;
25
+ export declare const DefaultChat: ({ className, title, instructions, runner, maxTurns, defaultOpen, persist, inline, }: ChatProps) => ReactNode;
23
26
  declare const _default: import("react").ComponentType<ChatProps>;
24
27
  export default _default;
@@ -10,7 +10,11 @@ export declare const endpointEntriesOf: (refName: string, fetch: FetchProxy) =>
10
10
  export declare const matchesSearch: (key: string, path: string, search: string) => boolean;
11
11
  /** `None` refuses every caller and is never badged, so it is not a name anyone filters by either. */
12
12
  export declare const guardsOf: (endpoint: SerializedEndpoint) => string[];
13
+ /** Naming no guard is `Public` spelled by omission — both are reachable by every caller, so the filter reads them
14
+ * as the same thing even though only the explicit one carries a badge. */
15
+ export declare const isPublicEndpoint: (endpoint: SerializedEndpoint) => boolean;
13
16
  /** Every guard name the serialized signals declare — the app's own authorization vocabulary, not a fixed role list. */
14
17
  export declare const guardNamesOf: (fetch: FetchProxy) => string[];
15
- /** No selection is no filter: the toolbar is optional, and an endpoint naming no guard is reachable by everyone. */
18
+ /** No selection is no filter, and a selection is a claim about the guard list so a public endpoint answers to
19
+ * `Public` alone rather than surviving every other filter. */
16
20
  export declare const matchesGuards: (endpoint: SerializedEndpoint, selected: string[]) => boolean;
package/ui/Agent/Chat.tsx CHANGED
@@ -2,7 +2,8 @@
2
2
  import { cn, fetch, usePage } from "akanjs/client";
3
3
  import type { PromptResult } from "akanjs/signal";
4
4
  import { AgentContext, type AgentPrompt, AgentPrompts, ensureStoreSurface, ScreenSettle } from "akanjs/store";
5
- import { useContext, useEffect, useRef, useState, useSyncExternalStore } from "react";
5
+ import { type ReactNode, useContext, useEffect, useRef, useState, useSyncExternalStore } from "react";
6
+ import { createPortal } from "react-dom";
6
7
  import { AiOutlineClear, AiOutlineClose } from "react-icons/ai";
7
8
  import { type AgentRunner, AgentSession, SessionContext } from "../../vendor/use-agentic";
8
9
  import { Button } from "../Button";
@@ -25,10 +26,14 @@ export interface ChatProps {
25
26
  defaultOpen?: boolean;
26
27
  /** Keeps the transcript across reloads — sessionStorage by default, `{ storage: "local" }` to outlive the tab. */
27
28
  persist?: PersistOption;
29
+ /** Renders in the page flow instead of floating above it — a zone chat that lives inside its own section. */
30
+ inline?: boolean;
28
31
  }
29
32
 
30
33
  const isApplePlatform = () => /Mac|iPhone|iPad|iPod/i.test(navigator.platform);
31
34
 
35
+ const floatingLayer = "fixed right-4 bottom-4 z-[150]";
36
+
32
37
  /**
33
38
  * The user-facing half of the in-page agent: one floating chat wired to the same surface the dock inspects.
34
39
  * The conversation loop runs in this browser session — every tool call executes here, gated by the approval
@@ -44,6 +49,7 @@ export const DefaultChat = ({
44
49
  maxTurns,
45
50
  defaultOpen = false,
46
51
  persist,
52
+ inline = false,
47
53
  }: ChatProps) => {
48
54
  const { l } = usePage();
49
55
  const provided = useContext(SessionContext);
@@ -71,6 +77,10 @@ export const DefaultChat = ({
71
77
  const [hotkey, setHotkey] = useState<{ label: string; keys: string } | null>(null);
72
78
  const listRef = useRef<HTMLDivElement>(null);
73
79
  const inputRef = useRef<HTMLInputElement>(null);
80
+ const [overlay, setOverlay] = useState<HTMLElement | null>(null);
81
+ useEffect(() => {
82
+ setOverlay(document.body);
83
+ }, []);
74
84
  useEffect(() => {
75
85
  listRef.current?.scrollTo({ top: listRef.current.scrollHeight });
76
86
  }, [version, open]);
@@ -153,14 +163,16 @@ export const DefaultChat = ({
153
163
  const orphans = (message.toolResults ?? []).filter((result) => !claimed.has(result.id));
154
164
  return orphans.length ? [<Bubble key={idx} message={{ ...message, toolResults: orphans }} />] : [];
155
165
  });
166
+ const layer = (surface: ReactNode) => (inline ? surface : overlay ? createPortal(surface, overlay) : null);
156
167
  if (!open)
157
- return (
168
+ return layer(
158
169
  <button
159
170
  aria-keyshortcuts={hotkey?.keys}
160
171
  aria-label={l("base.agent")}
161
172
  data-agent-ui=""
162
173
  className={cn(
163
- "group/agent fixed right-4 bottom-4 z-50 flex size-12 items-center justify-center rounded-full border border-primary/20 bg-primary/90 text-primary-foreground shadow-lg shadow-primary/30 transition-transform hover:scale-105",
174
+ "group/agent flex size-12 items-center justify-center rounded-full border border-primary/20 bg-primary/90 text-primary-foreground shadow-lg shadow-primary/30 transition-transform hover:scale-105",
175
+ !inline && floatingLayer,
164
176
  className,
165
177
  )}
166
178
  onClick={() => setOpen(true)}
@@ -183,14 +195,15 @@ export const DefaultChat = ({
183
195
  C78.2 29.2 72.8 23.8 60 22 C72.8 20.2 78.2 14.8 80 2 Z"
184
196
  />
185
197
  </svg>
186
- </button>
198
+ </button>,
187
199
  );
188
- return (
189
-
200
+
201
+ return layer(
190
202
  <aside
191
203
  data-agent-ui=""
192
204
  className={cn(
193
- "fixed right-4 bottom-4 z-50 flex h-[min(600px,calc(100dvh-2rem))] w-[min(24rem,calc(100vw-2rem))] flex-col overflow-hidden rounded-box border border-border bg-background shadow-xl",
205
+ "flex h-[min(600px,calc(100dvh-2rem))] w-[min(24rem,calc(100vw-2rem))] flex-col overflow-hidden rounded-box border border-border bg-background shadow-xl",
206
+ !inline && floatingLayer,
194
207
  className,
195
208
  )}
196
209
  >
@@ -274,7 +287,7 @@ export const DefaultChat = ({
274
287
  </Button>
275
288
  )}
276
289
  </div>
277
- </aside>
290
+ </aside>,
278
291
  );
279
292
  };
280
293
 
@@ -31,18 +31,29 @@ export const matchesSearch = (key: string, path: string, search: string) => {
31
31
  /** `None` refuses every caller and is never badged, so it is not a name anyone filters by either. */
32
32
  export const guardsOf = (endpoint: SerializedEndpoint) => endpoint.guards?.filter((guard) => guard !== "None") ?? [];
33
33
 
34
+ /** Naming no guard is `Public` spelled by omission — both are reachable by every caller, so the filter reads them
35
+ * as the same thing even though only the explicit one carries a badge. */
36
+ export const isPublicEndpoint = (endpoint: SerializedEndpoint) => {
37
+ const guards = guardsOf(endpoint);
38
+ return !guards.length || guards.every((guard) => guard === "Public");
39
+ };
40
+
34
41
  /** Every guard name the serialized signals declare — the app's own authorization vocabulary, not a fixed role list. */
35
42
  export const guardNamesOf = (fetch: FetchProxy) => {
36
43
  const names = new Set<string>();
37
44
  for (const refName of Object.keys(fetch.serializedSignal))
38
- for (const { endpoint } of endpointEntriesOf(refName, fetch))
45
+ for (const { endpoint } of endpointEntriesOf(refName, fetch)) {
46
+
47
+ if (isPublicEndpoint(endpoint)) names.add("Public");
39
48
  for (const guard of guardsOf(endpoint)) names.add(guard);
49
+ }
40
50
  return [...names].sort((a, b) => (a > b ? 1 : -1));
41
51
  };
42
52
 
43
- /** No selection is no filter: the toolbar is optional, and an endpoint naming no guard is reachable by everyone. */
53
+ /** No selection is no filter, and a selection is a claim about the guard list so a public endpoint answers to
54
+ * `Public` alone rather than surviving every other filter. */
44
55
  export const matchesGuards = (endpoint: SerializedEndpoint, selected: string[]) => {
45
56
  if (!selected.length) return true;
46
- const guards = guardsOf(endpoint);
47
- return !guards.length || guards.some((guard) => selected.includes(guard));
57
+ if (isPublicEndpoint(endpoint)) return selected.includes("Public");
58
+ return guardsOf(endpoint).some((guard) => selected.includes(guard));
48
59
  };