akanjs 3.0.0-alpha.56 → 3.0.0-alpha.57

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.56",
3
+ "version": "3.0.0-alpha.57",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -31,6 +31,11 @@ export interface ChatProps {
31
31
  * button, a menu item — instead of the built-in launcher. Left off, the panel owns the state as before.
32
32
  */
33
33
  open?: boolean;
34
+ /**
35
+ * Left off while `open` is controlled, the panel cannot close itself — so it draws **no close button** rather
36
+ * than an inert one. That is the shape of a fixed panel with nowhere to close to, and it is also what keeps a
37
+ * controlled chat assemblable by a server component, since this is the only prop here that is a function.
38
+ */
34
39
  onOpenChange?: (open: boolean) => void;
35
40
  /** `false` draws no launcher, for an app that opens the panel from a control of its own. */
36
41
  launcher?: boolean;
@@ -0,0 +1,20 @@
1
+ import { type AgentSessionOptions, type SessionHistory } from "../../vendor/use-agentic.d.ts";
2
+ export interface HistoryProps {
3
+ load: SessionHistory["load"];
4
+ save: SessionHistory["save"];
5
+ clear: SessionHistory["clear"];
6
+ /** Where a host with its own server-side summary moves its watermark — see `onCompact` on the session options. */
7
+ onCompact?: AgentSessionOptions["onCompact"];
8
+ }
9
+ /**
10
+ * Puts the enclosing zone's transcript wherever the app keeps it, as a mounted component rather than a prop.
11
+ *
12
+ * `persist` does the same thing and has to be passed to whoever builds the session, which makes every ancestor up
13
+ * to that point a client component — a function cannot cross the server/client boundary as a prop. Mounted here
14
+ * instead, the only client module an app needs is this leaf, and `Agent.Zone` and the chat inside it can be
15
+ * assembled by a server component. Same shape as `Agent.Guide`, and it renders nothing.
16
+ *
17
+ * Restoring follows the session's one rule: it lands only while nothing has happened to the conversation yet, so
18
+ * mounting with the zone restores and mounting later saves from there on.
19
+ */
20
+ export declare const History: ({ load, save, clear, onCompact }: HistoryProps) => null;
@@ -8,6 +8,7 @@ export declare const Agent: {
8
8
  Context: typeof Context;
9
9
  Dock: ({ className, bridge, surface, open }: import("./Dock.d.ts").DockProps) => import("react/jsx-runtime").JSX.Element;
10
10
  Guide: ({ instructions }: import("./Guide.d.ts").GuideProps) => null;
11
+ History: ({ load, save, clear, onCompact }: import("./History.d.ts").HistoryProps) => null;
11
12
  Scope: ({ id, label, kind, children }: import("../../vendor/use-agentic.d.ts").AgentScopeProps) => import("react/jsx-runtime").JSX.Element;
12
13
  Section: typeof Section;
13
14
  Skip: ({ className, label, children }: import("./Skip.d.ts").SkipProps) => import("react/jsx-runtime").JSX.Element;
@@ -8,6 +8,7 @@ export type { ChatProps } from "./Agent/Chat.d.ts";
8
8
  export { type ChatCommand, ChatCommands } from "./Agent/ChatCommands.d.ts";
9
9
  export { type ComposerProps, DefaultComposer } from "./Agent/Composer.d.ts";
10
10
  export { fetchRunner } from "./Agent/fetchRunner.d.ts";
11
+ export type { HistoryProps as AgentHistoryProps } from "./Agent/History.d.ts";
11
12
  export { DefaultLauncher, type LauncherProps } from "./Agent/Launcher.d.ts";
12
13
  export { type CodeProps, DefaultCode, DefaultMarkdown, type MarkdownProps } from "./Agent/Markdown.d.ts";
13
14
  export { DefaultMenu, type MenuProps as AgentMenuProps, type MenuRow } from "./Agent/Menu.d.ts";
@@ -127,6 +127,17 @@ export declare class AgentSession {
127
127
  * is one the winding-down turn appends onto. The returned promise waits for the history to clear too.
128
128
  */
129
129
  reset: () => Promise<void>;
130
+ /**
131
+ * Attaches a transcript store to a session built without one — what `Agent.History` mounts, so a zone can be
132
+ * assembled by a server component and still keep its transcript wherever the app keeps it. `null` detaches.
133
+ *
134
+ * Restoring follows the rule an async `load` already follows: it lands only while nothing has happened to this
135
+ * session yet. Attach before the first turn and it restores; attach after and it saves from there on, with the
136
+ * store never asked for a transcript that would be discarded. One rule rather than a mount-order surprise.
137
+ */
138
+ setHistory: (history: SessionHistory | null) => void;
139
+ /** The compaction hook as a setter, for the same reason `setHistory` is one: a host attaches it after the fact. */
140
+ setOnCompact: (onCompact: AgentSessionOptions["onCompact"] | null) => void;
130
141
  /**
131
142
  * Re-runs the last user message, dropping what the previous attempt produced. Turns fail for reasons that have
132
143
  * nothing to do with what was asked — a refused relay, a model that is unavailable — and retyping is otherwise the
package/ui/Agent/Chat.tsx CHANGED
@@ -69,6 +69,11 @@ export interface ChatProps {
69
69
  * button, a menu item — instead of the built-in launcher. Left off, the panel owns the state as before.
70
70
  */
71
71
  open?: boolean;
72
+ /**
73
+ * Left off while `open` is controlled, the panel cannot close itself — so it draws **no close button** rather
74
+ * than an inert one. That is the shape of a fixed panel with nowhere to close to, and it is also what keeps a
75
+ * controlled chat assemblable by a server component, since this is the only prop here that is a function.
76
+ */
72
77
  onOpenChange?: (open: boolean) => void;
73
78
  /** `false` draws no launcher, for an app that opens the panel from a control of its own. */
74
79
  launcher?: boolean;
@@ -0,0 +1,44 @@
1
+ "use client";
2
+ import { useContext, useEffect, useRef } from "react";
3
+ import { type AgentSessionOptions, SessionContext, type SessionHistory } from "../../vendor/use-agentic";
4
+
5
+ export interface HistoryProps {
6
+ load: SessionHistory["load"];
7
+ save: SessionHistory["save"];
8
+ clear: SessionHistory["clear"];
9
+ /** Where a host with its own server-side summary moves its watermark — see `onCompact` on the session options. */
10
+ onCompact?: AgentSessionOptions["onCompact"];
11
+ }
12
+
13
+ /**
14
+ * Puts the enclosing zone's transcript wherever the app keeps it, as a mounted component rather than a prop.
15
+ *
16
+ * `persist` does the same thing and has to be passed to whoever builds the session, which makes every ancestor up
17
+ * to that point a client component — a function cannot cross the server/client boundary as a prop. Mounted here
18
+ * instead, the only client module an app needs is this leaf, and `Agent.Zone` and the chat inside it can be
19
+ * assembled by a server component. Same shape as `Agent.Guide`, and it renders nothing.
20
+ *
21
+ * Restoring follows the session's one rule: it lands only while nothing has happened to the conversation yet, so
22
+ * mounting with the zone restores and mounting later saves from there on.
23
+ */
24
+ export const History = ({ load, save, clear, onCompact }: HistoryProps) => {
25
+ const session = useContext(SessionContext);
26
+ if (!session) throw new Error("Agent.History needs an enclosing Agent.Zone or AgentProvider to hold the session.");
27
+
28
+ const latest = useRef({ load, save, clear, onCompact });
29
+ latest.current = { load, save, clear, onCompact };
30
+ useEffect(() => {
31
+ session.setHistory({
32
+ load: () => latest.current.load(),
33
+ save: (messages) => latest.current.save(messages),
34
+ clear: () => latest.current.clear(),
35
+ });
36
+ session.setOnCompact((replaced, summary) => latest.current.onCompact?.(replaced, summary));
37
+ return () => {
38
+
39
+ session.setHistory(null);
40
+ session.setOnCompact(null);
41
+ };
42
+ }, [session]);
43
+ return null;
44
+ };
package/ui/Agent/index.ts CHANGED
@@ -2,6 +2,7 @@ import { AgentScope } from "../../vendor/use-agentic";
2
2
  import Context from "./Context";
3
3
  import { Dock } from "./Dock";
4
4
  import { Guide } from "./Guide";
5
+ import { History } from "./History";
5
6
  import { Chat } from "./index_";
6
7
  import Section from "./Section";
7
8
  import { Skip } from "./Skip";
@@ -15,6 +16,7 @@ export const Agent = {
15
16
  Context,
16
17
  Dock,
17
18
  Guide,
19
+ History,
18
20
  Scope: AgentScope,
19
21
  Section,
20
22
  Skip,
package/ui/index.ts CHANGED
@@ -27,6 +27,7 @@ export type { ChatProps } from "./Agent/Chat";
27
27
  export { type ChatCommand, ChatCommands } from "./Agent/ChatCommands";
28
28
  export { type ComposerProps, DefaultComposer } from "./Agent/Composer";
29
29
  export { fetchRunner } from "./Agent/fetchRunner";
30
+ export type { HistoryProps as AgentHistoryProps } from "./Agent/History";
30
31
  export { DefaultLauncher, type LauncherProps } from "./Agent/Launcher";
31
32
  export { type CodeProps, DefaultCode, DefaultMarkdown, type MarkdownProps } from "./Agent/Markdown";
32
33
  export { DefaultMenu, type MenuProps as AgentMenuProps, type MenuRow } from "./Agent/Menu";
@@ -139,6 +139,9 @@ export class AgentSession {
139
139
  #saveTimer: ReturnType<typeof setTimeout> | null = null;
140
140
  #saving: Promise<unknown> = Promise.resolve();
141
141
  #restoring = false;
142
+
143
+ #history: SessionHistory | undefined;
144
+ #onCompact: AgentSessionOptions["onCompact"];
142
145
  #compacting = false;
143
146
  /** Size below which auto-compaction stays out of the way, raised when a summary failed to shrink anything. */
144
147
  #compactFloor = 0;
@@ -147,6 +150,8 @@ export class AgentSession {
147
150
  this.#surface = surface;
148
151
  this.#runner = runner;
149
152
  this.#options = options;
153
+ this.#history = options.history;
154
+ this.#onCompact = options.onCompact;
150
155
  const restored = AgentSession.#restored(options.history);
151
156
  if (Array.isArray(restored)) this.#messages = restored;
152
157
  else {
@@ -291,7 +296,7 @@ export class AgentSession {
291
296
  clearTimeout(this.#saveTimer);
292
297
  this.#saveTimer = null;
293
298
  }
294
- const history = this.#options.history;
299
+ const history = this.#history;
295
300
  if (history) {
296
301
 
297
302
  this.#saving = this.#saving.then(() => history.clear()).catch(() => undefined);
@@ -301,6 +306,31 @@ export class AgentSession {
301
306
  for (const listener of this.#listeners) listener();
302
307
  };
303
308
 
309
+ /**
310
+ * Attaches a transcript store to a session built without one — what `Agent.History` mounts, so a zone can be
311
+ * assembled by a server component and still keep its transcript wherever the app keeps it. `null` detaches.
312
+ *
313
+ * Restoring follows the rule an async `load` already follows: it lands only while nothing has happened to this
314
+ * session yet. Attach before the first turn and it restores; attach after and it saves from there on, with the
315
+ * store never asked for a transcript that would be discarded. One rule rather than a mount-order surprise.
316
+ */
317
+ setHistory = (history: SessionHistory | null) => {
318
+ this.#history = history ?? undefined;
319
+ if (!history || this.#version !== 0) return;
320
+ const restored = AgentSession.#restored(history);
321
+ if (!(restored instanceof Promise)) {
322
+ this.#restore(restored);
323
+ return;
324
+ }
325
+ this.#restoring = true;
326
+ void this.#hydrate(restored);
327
+ };
328
+
329
+ /** The compaction hook as a setter, for the same reason `setHistory` is one: a host attaches it after the fact. */
330
+ setOnCompact = (onCompact: AgentSessionOptions["onCompact"] | null) => {
331
+ this.#onCompact = onCompact ?? undefined;
332
+ };
333
+
304
334
  /**
305
335
  * Re-runs the last user message, dropping what the previous attempt produced. Turns fail for reasons that have
306
336
  * nothing to do with what was asked — a refused relay, a model that is unavailable — and retyping is otherwise the
@@ -378,7 +408,7 @@ export class AgentSession {
378
408
  const message = Compaction.message(summary);
379
409
  this.#messages = [message, ...this.#messages.slice(at)];
380
410
  try {
381
- this.#options.onCompact?.(replaced, message);
411
+ this.#onCompact?.(replaced, message);
382
412
  } catch {
383
413
  }
384
414
  return true;
@@ -657,7 +687,7 @@ export class AgentSession {
657
687
 
658
688
  /** Debounced: streaming patches the last message on every delta, and a save per delta would thrash storage. */
659
689
  #schedulePersist() {
660
- const history = this.#options.history;
690
+ const history = this.#history;
661
691
  if (!history) return;
662
692
  if (this.#saveTimer) clearTimeout(this.#saveTimer);
663
693
  this.#saveTimer = setTimeout(() => {
@@ -696,6 +726,15 @@ export class AgentSession {
696
726
  } catch {
697
727
  }
698
728
  this.#restoring = false;
729
+ this.#restore(restored);
730
+ }
731
+
732
+ /**
733
+ * Lands a restore under the one rule — only into a session nothing has happened to yet — and notifies either
734
+ * way, because `isRestoring` may have turned over with it. Notifies by hand rather than through `#notify`,
735
+ * which would save the transcript it has just loaded.
736
+ */
737
+ #restore(restored: ChatMessage[]) {
699
738
  if (this.#version === 0 && restored.length) this.#messages = restored;
700
739
  this.#version += 1;
701
740
  for (const listener of this.#listeners) listener();