akanjs 3.0.0-alpha.56 → 3.0.0-alpha.58
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/client/csrTypes.ts +4 -3
- package/client/index.ts +1 -0
- package/client/locale.ts +2 -2
- package/client/sharedContext.ts +30 -0
- package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
- package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
- package/package.json +1 -1
- package/types/client/index.d.ts +1 -0
- package/types/client/sharedContext.d.ts +21 -0
- package/types/ui/Agent/Chat.d.ts +5 -0
- package/types/ui/Agent/History.d.ts +26 -0
- package/types/ui/Agent/index.d.ts +1 -0
- package/types/ui/Dialog/context.d.ts +1 -1
- package/types/ui/Tab/context.d.ts +1 -1
- package/types/ui/UiOverride/context.d.ts +1 -1
- package/types/ui/index.d.ts +1 -0
- package/types/vendor/use-agentic/AgentSession.d.ts +13 -0
- package/ui/Agent/Chat.tsx +5 -0
- package/ui/Agent/History.tsx +49 -0
- package/ui/Agent/index.ts +2 -0
- package/ui/Dialog/context.ts +3 -2
- package/ui/DragAction.tsx +3 -2
- package/ui/DraggableList.tsx +3 -2
- package/ui/ScreenNavigator.tsx +3 -2
- package/ui/Tab/context.ts +3 -2
- package/ui/UiOverride/context.ts +3 -2
- package/ui/index.ts +1 -0
- package/ui/overlayLayer.ts +3 -2
- package/vendor/use-agentic/AgentSession.ts +50 -3
package/client/csrTypes.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import type { ReactDOMAttributes } from "@use-gesture/react/dist/declarations/src/types";
|
|
3
3
|
import type { PromiseOrObject } from "akanjs/base";
|
|
4
|
-
import {
|
|
4
|
+
import { type ForwardRefExoticComponent, type ReactNode, type RefObject, useContext } from "react";
|
|
5
5
|
import type { AnimatedComponent, AnimatedProps, Interpolation, SpringValue } from "react-spring";
|
|
6
6
|
import type { RouterInstance } from "./router";
|
|
7
|
+
import { sharedContext } from "./sharedContext";
|
|
7
8
|
import type { ReactFont } from "./types";
|
|
8
9
|
|
|
9
10
|
export type TransitionType = "none" | "fade" | "bottomUp" | "stack" | "scaleOut";
|
|
@@ -349,7 +350,7 @@ export type UseCsrTransition = CsrTransitionStyles & {
|
|
|
349
350
|
};
|
|
350
351
|
|
|
351
352
|
export type CsrContextType = RouteState & UseCsrTransition;
|
|
352
|
-
export const csrContext =
|
|
353
|
+
export const csrContext = sharedContext<CsrContextType>("csr", {} as unknown as CsrContextType);
|
|
353
354
|
export const useCsr = () => {
|
|
354
355
|
const contextValues = useContext(csrContext);
|
|
355
356
|
return contextValues;
|
|
@@ -363,7 +364,7 @@ export interface PathContextType {
|
|
|
363
364
|
setGestureEnabled: (enabled: boolean) => void;
|
|
364
365
|
registerFrameSlot: (slot: FrameSlotRegistration) => () => void;
|
|
365
366
|
}
|
|
366
|
-
export const pathContext =
|
|
367
|
+
export const pathContext = sharedContext<PathContextType>("path", {} as unknown as PathContextType);
|
|
367
368
|
export const usePathCtx = () => {
|
|
368
369
|
const contextValues = useContext(pathContext);
|
|
369
370
|
return contextValues;
|
package/client/index.ts
CHANGED
package/client/locale.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
2
|
+
import { sharedContext } from "./sharedContext";
|
|
3
3
|
|
|
4
|
-
export const dictionaryContext =
|
|
4
|
+
export const dictionaryContext = sharedContext<{ [key: string]: { [key: string]: string } }>("dictionary", {});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type Context, createContext } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A React context interned on `globalThis`, which is how every context this framework owns must be made.
|
|
5
|
+
*
|
|
6
|
+
* An akan build does not give an app one copy of `akanjs`: it inlines each reachable module into every client
|
|
7
|
+
* chunk that reaches it — four copies of the agent runtime in this repo's own docs app. A context is identified
|
|
8
|
+
* by object identity, so a Provider mounted from one copy is invisible to a consumer holding another, and the
|
|
9
|
+
* consumer silently reads the context's default instead.
|
|
10
|
+
*
|
|
11
|
+
* Silently is the whole problem. `UiOverrideContext` was a plain `createContext`, so a route's `_overrides.tsx`
|
|
12
|
+
* bound its slots in the chunk holding the generated provider and every overridable component in another chunk
|
|
13
|
+
* kept rendering the framework default — no throw, no warning, and only *some* slots affected, which reads like
|
|
14
|
+
* a bug in the app's own component. The same shape cost a zone every tool it declared.
|
|
15
|
+
*
|
|
16
|
+
* The rule is therefore mechanical rather than case-by-case: a context whose Provider and consumers can be
|
|
17
|
+
* bundled apart is every context worth having, so all of them go through here. A test fails if one does not.
|
|
18
|
+
*
|
|
19
|
+
* `use-agentic` carries its own twin under a `useAgentic.` prefix, deliberately — it ships as a standalone
|
|
20
|
+
* package and cannot import this one. The prefixes keep the two namespaces from ever meeting.
|
|
21
|
+
*/
|
|
22
|
+
export const sharedContext = <T>(name: string, initial: T): Context<T> => {
|
|
23
|
+
const key = Symbol.for(`akanjs.context.${name}`);
|
|
24
|
+
const holder = globalThis as typeof globalThis & { [slot: symbol]: Context<T> | undefined };
|
|
25
|
+
const existing = holder[key];
|
|
26
|
+
if (existing) return existing;
|
|
27
|
+
const created = createContext(initial);
|
|
28
|
+
holder[key] = created;
|
|
29
|
+
return created;
|
|
30
|
+
};
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
package/types/client/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export * from "./locale.d.ts";
|
|
|
11
11
|
export * from "./makePageProto.d.ts";
|
|
12
12
|
export * from "./router.d.ts";
|
|
13
13
|
export * from "./rscNavigation.d.ts";
|
|
14
|
+
export * from "./sharedContext.d.ts";
|
|
14
15
|
export * from "./storage.d.ts";
|
|
15
16
|
export * from "./translator.d.ts";
|
|
16
17
|
export * from "./types.d.ts";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type Context } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* A React context interned on `globalThis`, which is how every context this framework owns must be made.
|
|
4
|
+
*
|
|
5
|
+
* An akan build does not give an app one copy of `akanjs`: it inlines each reachable module into every client
|
|
6
|
+
* chunk that reaches it — four copies of the agent runtime in this repo's own docs app. A context is identified
|
|
7
|
+
* by object identity, so a Provider mounted from one copy is invisible to a consumer holding another, and the
|
|
8
|
+
* consumer silently reads the context's default instead.
|
|
9
|
+
*
|
|
10
|
+
* Silently is the whole problem. `UiOverrideContext` was a plain `createContext`, so a route's `_overrides.tsx`
|
|
11
|
+
* bound its slots in the chunk holding the generated provider and every overridable component in another chunk
|
|
12
|
+
* kept rendering the framework default — no throw, no warning, and only *some* slots affected, which reads like
|
|
13
|
+
* a bug in the app's own component. The same shape cost a zone every tool it declared.
|
|
14
|
+
*
|
|
15
|
+
* The rule is therefore mechanical rather than case-by-case: a context whose Provider and consumers can be
|
|
16
|
+
* bundled apart is every context worth having, so all of them go through here. A test fails if one does not.
|
|
17
|
+
*
|
|
18
|
+
* `use-agentic` carries its own twin under a `useAgentic.` prefix, deliberately — it ships as a standalone
|
|
19
|
+
* package and cannot import this one. The prefixes keep the two namespaces from ever meeting.
|
|
20
|
+
*/
|
|
21
|
+
export declare const sharedContext: <T>(name: string, initial: T) => Context<T>;
|
package/types/ui/Agent/Chat.d.ts
CHANGED
|
@@ -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,26 @@
|
|
|
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
|
+
* The store is attached for exactly as long as this is mounted. A zone's own session dies with it, so that is the
|
|
21
|
+
* whole story there — but a session the app handed in (`Agent.Zone`'s `session` prop) outlives this, and its
|
|
22
|
+
* saving stops on unmount: nothing is claiming to persist it any more, and a component that is gone should not
|
|
23
|
+
* still be writing. A host that wants the store to outlive the view calls `session.setHistory` itself, which also
|
|
24
|
+
* takes the slot, so a later unmount here leaves it alone.
|
|
25
|
+
*/
|
|
26
|
+
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;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ComponentType } from "react";
|
|
2
2
|
import type { ClassNameValue as ClassValue } from "tailwind-merge";
|
|
3
3
|
import type { ApprovalProps as AgentApprovalProps } from "../Agent/Approval.d.ts";
|
|
4
4
|
import type { BubbleProps as AgentBubbleProps } from "../Agent/Bubble.d.ts";
|
package/types/ui/index.d.ts
CHANGED
|
@@ -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,19 @@ 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
|
+
* Returns the detach, which clears the slot only while this call's store is still the one in it.
|
|
139
|
+
*/
|
|
140
|
+
setHistory: (history: SessionHistory | null) => () => void;
|
|
141
|
+
/** The compaction hook as a setter, for the same reason `setHistory` is one: a host attaches it after the fact. */
|
|
142
|
+
setOnCompact: (onCompact: AgentSessionOptions["onCompact"] | null) => () => void;
|
|
130
143
|
/**
|
|
131
144
|
* Re-runs the last user message, dropping what the previous attempt produced. Turns fail for reasons that have
|
|
132
145
|
* 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,49 @@
|
|
|
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
|
+
* The store is attached for exactly as long as this is mounted. A zone's own session dies with it, so that is the
|
|
25
|
+
* whole story there — but a session the app handed in (`Agent.Zone`'s `session` prop) outlives this, and its
|
|
26
|
+
* saving stops on unmount: nothing is claiming to persist it any more, and a component that is gone should not
|
|
27
|
+
* still be writing. A host that wants the store to outlive the view calls `session.setHistory` itself, which also
|
|
28
|
+
* takes the slot, so a later unmount here leaves it alone.
|
|
29
|
+
*/
|
|
30
|
+
export const History = ({ load, save, clear, onCompact }: HistoryProps) => {
|
|
31
|
+
const session = useContext(SessionContext);
|
|
32
|
+
if (!session) throw new Error("Agent.History needs an enclosing Agent.Zone or AgentProvider to hold the session.");
|
|
33
|
+
|
|
34
|
+
const latest = useRef({ load, save, clear, onCompact });
|
|
35
|
+
latest.current = { load, save, clear, onCompact };
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const detachHistory = session.setHistory({
|
|
38
|
+
load: () => latest.current.load(),
|
|
39
|
+
save: (messages) => latest.current.save(messages),
|
|
40
|
+
clear: () => latest.current.clear(),
|
|
41
|
+
});
|
|
42
|
+
const detachCompact = session.setOnCompact((replaced, summary) => latest.current.onCompact?.(replaced, summary));
|
|
43
|
+
return () => {
|
|
44
|
+
detachHistory();
|
|
45
|
+
detachCompact();
|
|
46
|
+
};
|
|
47
|
+
}, [session]);
|
|
48
|
+
return null;
|
|
49
|
+
};
|
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/Dialog/context.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { sharedContext } from "../../client/sharedContext";
|
|
3
4
|
|
|
4
5
|
export interface DialogContextType {
|
|
5
6
|
open: boolean;
|
|
@@ -18,7 +19,7 @@ export interface DialogContextType {
|
|
|
18
19
|
setAction: (action: ReactNode) => void;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
export const DialogContext =
|
|
22
|
+
export const DialogContext = sharedContext<DialogContextType>("dialog", {
|
|
22
23
|
open: false,
|
|
23
24
|
setOpen: (open: boolean) => null,
|
|
24
25
|
openDialog: () => null,
|
package/ui/DragAction.tsx
CHANGED
|
@@ -3,8 +3,9 @@ import { useGesture } from "@use-gesture/react";
|
|
|
3
3
|
import type { ReactDOMAttributes } from "@use-gesture/react/dist/declarations/src/types";
|
|
4
4
|
import { cn } from "akanjs/client";
|
|
5
5
|
import { animated } from "akanjs/ui";
|
|
6
|
-
import {
|
|
6
|
+
import { type ReactNode, useContext, useRef } from "react";
|
|
7
7
|
import { SpringValue, useSpring } from "react-spring";
|
|
8
|
+
import { sharedContext } from "../client/sharedContext";
|
|
8
9
|
|
|
9
10
|
interface DragActionContextType {
|
|
10
11
|
bind: () => ReactDOMAttributes;
|
|
@@ -17,7 +18,7 @@ interface DragActionContextType {
|
|
|
17
18
|
onClick?: () => void;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
const DragActionContext =
|
|
21
|
+
const DragActionContext = sharedContext<DragActionContextType>("dragAction", {
|
|
21
22
|
bind: () => ({}),
|
|
22
23
|
x: new SpringValue(0),
|
|
23
24
|
y: new SpringValue(0),
|
package/ui/DraggableList.tsx
CHANGED
|
@@ -4,9 +4,10 @@ import { useGesture } from "@use-gesture/react";
|
|
|
4
4
|
import { cn } from "akanjs/client";
|
|
5
5
|
import { useFieldTool } from "akanjs/store";
|
|
6
6
|
import { animated } from "akanjs/ui";
|
|
7
|
-
import {
|
|
7
|
+
import { type ReactElement, type ReactNode, useContext, useRef } from "react";
|
|
8
8
|
import { BiTrash } from "react-icons/bi";
|
|
9
9
|
import { MdDragIndicator } from "react-icons/md";
|
|
10
|
+
import { sharedContext } from "../client/sharedContext";
|
|
10
11
|
import { agentAttrs } from "./agentAttrs";
|
|
11
12
|
import { buttonRecipe } from "./Button";
|
|
12
13
|
import { useUiRecipe } from "./UiOverride";
|
|
@@ -24,7 +25,7 @@ interface DragListContextType<V> {
|
|
|
24
25
|
bind: (...args: any[]) => any;
|
|
25
26
|
onRemove: (value: V) => void;
|
|
26
27
|
}
|
|
27
|
-
const dragListContext =
|
|
28
|
+
const dragListContext = sharedContext<DragListContextType<any>>("dragList", {} as unknown as DragListContextType<any>);
|
|
28
29
|
const useDragList = () => useContext(dragListContext);
|
|
29
30
|
|
|
30
31
|
interface DragListProps<V> {
|
package/ui/ScreenNavigator.tsx
CHANGED
|
@@ -4,8 +4,9 @@ import { cn } from "akanjs/client";
|
|
|
4
4
|
import { capitalize } from "akanjs/common";
|
|
5
5
|
import { st } from "akanjs/store";
|
|
6
6
|
import { animated } from "akanjs/ui";
|
|
7
|
-
import {
|
|
7
|
+
import { type ReactNode, useContext, useEffect, useRef, useState } from "react";
|
|
8
8
|
import { SpringValue, useSpringValue } from "react-spring";
|
|
9
|
+
import { sharedContext } from "../client/sharedContext";
|
|
9
10
|
|
|
10
11
|
interface ScreenNavigatorContextType {
|
|
11
12
|
bind: (...args: any[]) => any;
|
|
@@ -16,7 +17,7 @@ interface ScreenNavigatorContextType {
|
|
|
16
17
|
onClickMenu: (menu: string) => void;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
const ScreenNavigatorContext =
|
|
20
|
+
const ScreenNavigatorContext = sharedContext<ScreenNavigatorContextType>("screenNavigator", {
|
|
20
21
|
bind: () => ({}),
|
|
21
22
|
xValue: new SpringValue(0),
|
|
22
23
|
setMenu: null as unknown as (menu: string) => void,
|
package/ui/Tab/context.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
2
|
+
import type { RefObject } from "react";
|
|
3
|
+
import { sharedContext } from "../../client/sharedContext";
|
|
3
4
|
|
|
4
5
|
interface TabContextType {
|
|
5
6
|
defaultMenu: string | null;
|
|
@@ -10,7 +11,7 @@ interface TabContextType {
|
|
|
10
11
|
switchTab: (menu: string) => void;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
export const TabContext =
|
|
14
|
+
export const TabContext = sharedContext<TabContextType>("tab", {
|
|
14
15
|
defaultMenu: null,
|
|
15
16
|
menu: null,
|
|
16
17
|
setMenu: (value: string | null) => null,
|
package/ui/UiOverride/context.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
2
|
+
import type { ComponentType } from "react";
|
|
3
3
|
import type { ClassNameValue as ClassValue } from "tailwind-merge";
|
|
4
|
+
import { sharedContext } from "../../client/sharedContext";
|
|
4
5
|
import type { ApprovalProps as AgentApprovalProps } from "../Agent/Approval";
|
|
5
6
|
import type { BubbleProps as AgentBubbleProps } from "../Agent/Bubble";
|
|
6
7
|
import type { ChatProps as AgentChatProps } from "../Agent/Chat";
|
|
@@ -130,4 +131,4 @@ export type AkanUiOverrideManifest = Partial<AkanUiOverrides> & { recipes?: Part
|
|
|
130
131
|
* merged (child wins) by each nested `UiOverrideProvider`, mirroring how nested
|
|
131
132
|
* `_layout.tsx` / `_overrides.tsx` stack down the route tree.
|
|
132
133
|
*/
|
|
133
|
-
export const UiOverrideContext =
|
|
134
|
+
export const UiOverrideContext = sharedContext<AkanUiOverrideManifest>("uiOverride", {});
|
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";
|
package/ui/overlayLayer.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
2
|
+
import { useContext } from "react";
|
|
3
|
+
import { sharedContext } from "../client/sharedContext";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Overlay surfaces render through `createPortal(document.body)`, so they leave the DOM subtree of the
|
|
@@ -22,7 +23,7 @@ export const OVERLAY_LAYER_ATTR = "data-akan-overlay";
|
|
|
22
23
|
export const overlayZ = { select: 90, dropdown: 100, popconfirmScrim: 105, popconfirm: 110 } as const;
|
|
23
24
|
|
|
24
25
|
/** Scope that rendered the surrounding content — empty at page level. */
|
|
25
|
-
const OverlayOwnerContext =
|
|
26
|
+
const OverlayOwnerContext = sharedContext("overlayOwner", "");
|
|
26
27
|
|
|
27
28
|
/** Wrap the content a dismissable container owns, with the scope from {@link useOverlayScope}. */
|
|
28
29
|
export const OverlayOwnerProvider = OverlayOwnerContext.Provider;
|
|
@@ -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.#
|
|
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,39 @@ 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
|
+
* Returns the detach, which clears the slot only while this call's store is still the one in it.
|
|
318
|
+
*/
|
|
319
|
+
setHistory = (history: SessionHistory | null) => {
|
|
320
|
+
this.#history = history ?? undefined;
|
|
321
|
+
if (history && this.#version === 0) {
|
|
322
|
+
const restored = AgentSession.#restored(history);
|
|
323
|
+
if (restored instanceof Promise) {
|
|
324
|
+
this.#restoring = true;
|
|
325
|
+
void this.#hydrate(restored);
|
|
326
|
+
} else this.#restore(restored);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return () => {
|
|
330
|
+
if (history && this.#history === history) this.#history = undefined;
|
|
331
|
+
};
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
/** The compaction hook as a setter, for the same reason `setHistory` is one: a host attaches it after the fact. */
|
|
335
|
+
setOnCompact = (onCompact: AgentSessionOptions["onCompact"] | null) => {
|
|
336
|
+
this.#onCompact = onCompact ?? undefined;
|
|
337
|
+
return () => {
|
|
338
|
+
if (onCompact && this.#onCompact === onCompact) this.#onCompact = undefined;
|
|
339
|
+
};
|
|
340
|
+
};
|
|
341
|
+
|
|
304
342
|
/**
|
|
305
343
|
* Re-runs the last user message, dropping what the previous attempt produced. Turns fail for reasons that have
|
|
306
344
|
* nothing to do with what was asked — a refused relay, a model that is unavailable — and retyping is otherwise the
|
|
@@ -378,7 +416,7 @@ export class AgentSession {
|
|
|
378
416
|
const message = Compaction.message(summary);
|
|
379
417
|
this.#messages = [message, ...this.#messages.slice(at)];
|
|
380
418
|
try {
|
|
381
|
-
this.#
|
|
419
|
+
this.#onCompact?.(replaced, message);
|
|
382
420
|
} catch {
|
|
383
421
|
}
|
|
384
422
|
return true;
|
|
@@ -657,7 +695,7 @@ export class AgentSession {
|
|
|
657
695
|
|
|
658
696
|
/** Debounced: streaming patches the last message on every delta, and a save per delta would thrash storage. */
|
|
659
697
|
#schedulePersist() {
|
|
660
|
-
const history = this.#
|
|
698
|
+
const history = this.#history;
|
|
661
699
|
if (!history) return;
|
|
662
700
|
if (this.#saveTimer) clearTimeout(this.#saveTimer);
|
|
663
701
|
this.#saveTimer = setTimeout(() => {
|
|
@@ -696,6 +734,15 @@ export class AgentSession {
|
|
|
696
734
|
} catch {
|
|
697
735
|
}
|
|
698
736
|
this.#restoring = false;
|
|
737
|
+
this.#restore(restored);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Lands a restore under the one rule — only into a session nothing has happened to yet — and notifies either
|
|
742
|
+
* way, because `isRestoring` may have turned over with it. Notifies by hand rather than through `#notify`,
|
|
743
|
+
* which would save the transcript it has just loaded.
|
|
744
|
+
*/
|
|
745
|
+
#restore(restored: ChatMessage[]) {
|
|
699
746
|
if (this.#version === 0 && restored.length) this.#messages = restored;
|
|
700
747
|
this.#version += 1;
|
|
701
748
|
for (const listener of this.#listeners) listener();
|