akanjs 3.0.0-alpha.57 → 3.0.0-alpha.59
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 +31 -0
- package/client/translator.ts +2 -2
- package/common/index.ts +1 -0
- package/common/pathGetLoose.ts +31 -0
- package/dictionary/DictionaryLookup.ts +2 -2
- package/dictionary/trans.ts +2 -2
- 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/service/predefinedAdaptor/database.adaptor.ts +17 -1
- package/types/client/index.d.ts +1 -0
- package/types/client/sharedContext.d.ts +21 -0
- package/types/common/index.d.ts +1 -0
- package/types/common/pathGetLoose.d.ts +8 -0
- package/types/ui/Agent/History.d.ts +6 -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/vendor/use-agentic/AgentSession.d.ts +4 -2
- package/ui/Agent/History.tsx +10 -5
- 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/Signal/Object.tsx +1 -1
- package/ui/Signal/makeExample.ts +9 -2
- package/ui/Tab/context.ts +3 -2
- package/ui/UiOverride/context.ts +3 -2
- package/ui/overlayLayer.ts +3 -2
- package/vendor/use-agentic/AgentSession.ts +15 -7
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,31 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { type Context, createContext } from "react";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A React context interned on `globalThis`, which is how every context this framework owns must be made.
|
|
6
|
+
*
|
|
7
|
+
* An akan build does not give an app one copy of `akanjs`: it inlines each reachable module into every client
|
|
8
|
+
* chunk that reaches it — four copies of the agent runtime in this repo's own docs app. A context is identified
|
|
9
|
+
* by object identity, so a Provider mounted from one copy is invisible to a consumer holding another, and the
|
|
10
|
+
* consumer silently reads the context's default instead.
|
|
11
|
+
*
|
|
12
|
+
* Silently is the whole problem. `UiOverrideContext` was a plain `createContext`, so a route's `_overrides.tsx`
|
|
13
|
+
* bound its slots in the chunk holding the generated provider and every overridable component in another chunk
|
|
14
|
+
* kept rendering the framework default — no throw, no warning, and only *some* slots affected, which reads like
|
|
15
|
+
* a bug in the app's own component. The same shape cost a zone every tool it declared.
|
|
16
|
+
*
|
|
17
|
+
* The rule is therefore mechanical rather than case-by-case: a context whose Provider and consumers can be
|
|
18
|
+
* bundled apart is every context worth having, so all of them go through here. A test fails if one does not.
|
|
19
|
+
*
|
|
20
|
+
* `use-agentic` carries its own twin under a `useAgentic.` prefix, deliberately — it ships as a standalone
|
|
21
|
+
* package and cannot import this one. The prefixes keep the two namespaces from ever meeting.
|
|
22
|
+
*/
|
|
23
|
+
export const sharedContext = <T>(name: string, initial: T): Context<T> => {
|
|
24
|
+
const key = Symbol.for(`akanjs.context.${name}`);
|
|
25
|
+
const holder = globalThis as typeof globalThis & { [slot: symbol]: Context<T> | undefined };
|
|
26
|
+
const existing = holder[key];
|
|
27
|
+
if (existing) return existing;
|
|
28
|
+
const created = createContext(initial);
|
|
29
|
+
holder[key] = created;
|
|
30
|
+
return created;
|
|
31
|
+
};
|
package/client/translator.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { pathGetLoose } from "akanjs/common";
|
|
2
2
|
|
|
3
3
|
export interface Dictionary {
|
|
4
4
|
[key: string]: {
|
|
@@ -63,7 +63,7 @@ export class Translator {
|
|
|
63
63
|
static translateByLocale(lang: string, key: string, param?: Record<string, string | number>): string {
|
|
64
64
|
const dictionary = getTranslatorState().langDictionaryMap.get(lang);
|
|
65
65
|
if (!dictionary) return key;
|
|
66
|
-
const msg = (
|
|
66
|
+
const msg = (pathGetLoose(key, dictionary, ".", { t: key }) as { t: string }).t;
|
|
67
67
|
return param ? msg.replace(/{([^}]+)}/g, (_, key: string) => param[key] as string) : msg;
|
|
68
68
|
}
|
|
69
69
|
|
package/common/index.ts
CHANGED
|
@@ -39,6 +39,7 @@ export {
|
|
|
39
39
|
export { mergeVersion } from "./mergeVersion";
|
|
40
40
|
export { objectify } from "./objectify";
|
|
41
41
|
export { pathGet } from "./pathGet";
|
|
42
|
+
export { pathGetLoose } from "./pathGetLoose";
|
|
42
43
|
export { pathSet } from "./pathSet";
|
|
43
44
|
export { randomPick } from "./randomPick";
|
|
44
45
|
export { randomPicks } from "./randomPicks";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
type Indexable = Record<string, unknown>;
|
|
2
|
+
|
|
3
|
+
const isIndexable = (value: unknown): value is Indexable => Object(value) === value;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Reads a dotted path whose segments may themselves contain the separator.
|
|
7
|
+
*
|
|
8
|
+
* Dictionary keys are built as `<refName>.<value>` and an enum value is a real-world identifier — `gpt-5.6-terra`,
|
|
9
|
+
* `v1.2` — so the key is not a clean dotted path and `pathGet` splits it into segments that were never nodes.
|
|
10
|
+
* The tree stores such a key literally, so resolution has to try joined prefixes too.
|
|
11
|
+
*/
|
|
12
|
+
export const pathGetLoose = (
|
|
13
|
+
path: string | readonly string[],
|
|
14
|
+
obj: unknown,
|
|
15
|
+
separator = ".",
|
|
16
|
+
fallback: unknown = null,
|
|
17
|
+
): unknown => {
|
|
18
|
+
const walk = (node: unknown, rest: readonly string[]): unknown => {
|
|
19
|
+
if (!rest.length) return node;
|
|
20
|
+
if (!isIndexable(node)) return undefined;
|
|
21
|
+
|
|
22
|
+
for (let take = 1; take <= rest.length; take += 1) {
|
|
23
|
+
const child = node[rest.slice(0, take).join(separator)];
|
|
24
|
+
if (child === undefined) continue;
|
|
25
|
+
const found = walk(child, rest.slice(take));
|
|
26
|
+
if (found !== undefined) return found;
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
};
|
|
30
|
+
return walk(obj, Array.isArray(path) ? [...path] : (path as string).split(separator)) ?? fallback;
|
|
31
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { pathGetLoose } from "akanjs/common";
|
|
2
2
|
import { DictionaryRegistry } from "./dictionaryRegistry";
|
|
3
3
|
import type { DictionaryNode } from "./trans";
|
|
4
4
|
|
|
@@ -23,7 +23,7 @@ export class DictionaryLookup {
|
|
|
23
23
|
const [refName, ...rest] = key.split(".");
|
|
24
24
|
if (!refName) return undefined;
|
|
25
25
|
const model = this.#models[refName];
|
|
26
|
-
const node = (rest.length ?
|
|
26
|
+
const node = (rest.length ? pathGetLoose(rest, model) : model) as { t?: unknown } | null;
|
|
27
27
|
const text = node?.t;
|
|
28
28
|
return typeof text === "string" && text.length ? text : undefined;
|
|
29
29
|
}
|
package/dictionary/trans.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { GetStateObject, ObjectAssign, Prettify } from "akanjs/base";
|
|
2
|
-
import {
|
|
2
|
+
import { pathGetLoose } from "akanjs/common";
|
|
3
3
|
|
|
4
4
|
import { DictionaryRegistry } from "./dictionaryRegistry";
|
|
5
5
|
import type { DictModule } from "./locale";
|
|
@@ -177,7 +177,7 @@ export const makeTrans = <
|
|
|
177
177
|
const msgKey = msgKeys.join(".");
|
|
178
178
|
const langDict = rootDictionary[lang] ?? {};
|
|
179
179
|
const model = langDict[modelName as string] ?? {};
|
|
180
|
-
const message =
|
|
180
|
+
const message = pathGetLoose(msgKey as string, model, ".", { t: key }) as { t: string };
|
|
181
181
|
return message.t;
|
|
182
182
|
};
|
|
183
183
|
const getDictionary = (lang: Language) => {
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -981,6 +981,7 @@ export class SqlDocumentStore {
|
|
|
981
981
|
#insertStmt: AkanSqlStatement | null = null;
|
|
982
982
|
#readStmtCache = new Map<string, AkanSqlStatement>();
|
|
983
983
|
#docPrototype: object | null = null;
|
|
984
|
+
#immutableKeys: string[] | null = null;
|
|
984
985
|
|
|
985
986
|
constructor(
|
|
986
987
|
private readonly owner: DocumentDatabaseOwner,
|
|
@@ -1483,7 +1484,9 @@ export class SqlDocumentStore {
|
|
|
1483
1484
|
originalData: DocumentRecord,
|
|
1484
1485
|
{ runSaveHooks = true, crudType = "update" }: WriteHookOptions = {},
|
|
1485
1486
|
) {
|
|
1486
|
-
const
|
|
1487
|
+
const prepared = this.prepareDocument({ ...data, id, updatedAt: dayjs() });
|
|
1488
|
+
this.#assertImmutableUnchanged(prepared, originalData);
|
|
1489
|
+
const doc = this.hydrate(prepared, originalData);
|
|
1487
1490
|
if (runSaveHooks) await this.runHooks("save", crudType, doc, "pre");
|
|
1488
1491
|
await this.runHooks(crudType, crudType, doc, "pre");
|
|
1489
1492
|
const row = this.toRow(doc);
|
|
@@ -1498,6 +1501,19 @@ export class SqlDocumentStore {
|
|
|
1498
1501
|
return doc;
|
|
1499
1502
|
}
|
|
1500
1503
|
|
|
1504
|
+
#assertImmutableUnchanged(prepared: DocumentRecord, originalData: DocumentRecord) {
|
|
1505
|
+
this.#immutableKeys ??= Object.entries(this.database.doc[FIELD_META] as unknown as FieldMap)
|
|
1506
|
+
.filter(([, fieldMeta]) => fieldMeta.getProps().immutable)
|
|
1507
|
+
.map(([key]) => key);
|
|
1508
|
+
if (!this.#immutableKeys.length) return;
|
|
1509
|
+
const changed = this.#immutableKeys.filter((key) => jsonStr(prepared[key]) !== jsonStr(originalData[key]));
|
|
1510
|
+
if (!changed.length) return;
|
|
1511
|
+
|
|
1512
|
+
throw new Error(
|
|
1513
|
+
`Cannot modify immutable field${changed.length > 1 ? "s" : ""} on ${this.table} (${String(prepared.id)}): ${changed.join(", ")}`,
|
|
1514
|
+
);
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1501
1517
|
private parseProjectedValue(value: unknown) {
|
|
1502
1518
|
if (typeof value !== "string") return value;
|
|
1503
1519
|
const trimmed = value.trim();
|
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/common/index.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export { isMcpDescribableArg, type McpExposureEndpoint, type McpExposureOption,
|
|
|
21
21
|
export { mergeVersion } from "./mergeVersion.d.ts";
|
|
22
22
|
export { objectify } from "./objectify.d.ts";
|
|
23
23
|
export { pathGet } from "./pathGet.d.ts";
|
|
24
|
+
export { pathGetLoose } from "./pathGetLoose.d.ts";
|
|
24
25
|
export { pathSet } from "./pathSet.d.ts";
|
|
25
26
|
export { randomPick } from "./randomPick.d.ts";
|
|
26
27
|
export { randomPicks } from "./randomPicks.d.ts";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads a dotted path whose segments may themselves contain the separator.
|
|
3
|
+
*
|
|
4
|
+
* Dictionary keys are built as `<refName>.<value>` and an enum value is a real-world identifier — `gpt-5.6-terra`,
|
|
5
|
+
* `v1.2` — so the key is not a clean dotted path and `pathGet` splits it into segments that were never nodes.
|
|
6
|
+
* The tree stores such a key literally, so resolution has to try joined prefixes too.
|
|
7
|
+
*/
|
|
8
|
+
export declare const pathGetLoose: (path: string | readonly string[], obj: unknown, separator?: string, fallback?: unknown) => unknown;
|
|
@@ -16,5 +16,11 @@ export interface HistoryProps {
|
|
|
16
16
|
*
|
|
17
17
|
* Restoring follows the session's one rule: it lands only while nothing has happened to the conversation yet, so
|
|
18
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.
|
|
19
25
|
*/
|
|
20
26
|
export declare const History: ({ load, save, clear, onCompact }: HistoryProps) => null;
|
|
@@ -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";
|
|
@@ -134,10 +134,12 @@ export declare class AgentSession {
|
|
|
134
134
|
* Restoring follows the rule an async `load` already follows: it lands only while nothing has happened to this
|
|
135
135
|
* session yet. Attach before the first turn and it restores; attach after and it saves from there on, with the
|
|
136
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.
|
|
137
139
|
*/
|
|
138
|
-
setHistory: (history: SessionHistory | null) => void;
|
|
140
|
+
setHistory: (history: SessionHistory | null) => () => void;
|
|
139
141
|
/** 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;
|
|
142
|
+
setOnCompact: (onCompact: AgentSessionOptions["onCompact"] | null) => () => void;
|
|
141
143
|
/**
|
|
142
144
|
* Re-runs the last user message, dropping what the previous attempt produced. Turns fail for reasons that have
|
|
143
145
|
* nothing to do with what was asked — a refused relay, a model that is unavailable — and retyping is otherwise the
|
package/ui/Agent/History.tsx
CHANGED
|
@@ -20,6 +20,12 @@ export interface HistoryProps {
|
|
|
20
20
|
*
|
|
21
21
|
* Restoring follows the session's one rule: it lands only while nothing has happened to the conversation yet, so
|
|
22
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.
|
|
23
29
|
*/
|
|
24
30
|
export const History = ({ load, save, clear, onCompact }: HistoryProps) => {
|
|
25
31
|
const session = useContext(SessionContext);
|
|
@@ -28,16 +34,15 @@ export const History = ({ load, save, clear, onCompact }: HistoryProps) => {
|
|
|
28
34
|
const latest = useRef({ load, save, clear, onCompact });
|
|
29
35
|
latest.current = { load, save, clear, onCompact };
|
|
30
36
|
useEffect(() => {
|
|
31
|
-
session.setHistory({
|
|
37
|
+
const detachHistory = session.setHistory({
|
|
32
38
|
load: () => latest.current.load(),
|
|
33
39
|
save: (messages) => latest.current.save(messages),
|
|
34
40
|
clear: () => latest.current.clear(),
|
|
35
41
|
});
|
|
36
|
-
session.setOnCompact((replaced, summary) => latest.current.onCompact?.(replaced, summary));
|
|
42
|
+
const detachCompact = session.setOnCompact((replaced, summary) => latest.current.onCompact?.(replaced, summary));
|
|
37
43
|
return () => {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
session.setOnCompact(null);
|
|
44
|
+
detachHistory();
|
|
45
|
+
detachCompact();
|
|
41
46
|
};
|
|
42
47
|
}, [session]);
|
|
43
48
|
return null;
|
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/Signal/Object.tsx
CHANGED
|
@@ -90,7 +90,7 @@ const ObjectDetail = ({ className, objRef }: ObjectDetailProps) => {
|
|
|
90
90
|
<ObjectType objRef={modelRef} arrDepth={arrDepth} nullable={nullable} />
|
|
91
91
|
) : (
|
|
92
92
|
<span className={docPill("muted", "font-mono")}>
|
|
93
|
-
{typeLabel(ConstantRegistry.getModelName(modelRef), arrDepth, nullable)}
|
|
93
|
+
{typeLabel(isMap ? "Map" : ConstantRegistry.getModelName(modelRef), arrDepth, nullable)}
|
|
94
94
|
</span>
|
|
95
95
|
)}
|
|
96
96
|
{isMap ? (
|
package/ui/Signal/makeExample.ts
CHANGED
|
@@ -8,10 +8,15 @@ import {
|
|
|
8
8
|
PrimitiveRegistry,
|
|
9
9
|
type PrimitiveScalar,
|
|
10
10
|
} from "akanjs/base";
|
|
11
|
-
import { type ConstantCls, ConstantRegistry } from "akanjs/constant";
|
|
11
|
+
import { type ConstantCls, type ConstantField, ConstantRegistry } from "akanjs/constant";
|
|
12
12
|
|
|
13
13
|
import type { SerializedArg, SerializedEndpoint, SignalType } from "akanjs/signal";
|
|
14
14
|
|
|
15
|
+
const getMapExample = (field: ConstantField, getValueExample: (modelRef: Cls) => unknown) => {
|
|
16
|
+
const [valueRef, valueArrDepth] = getNonArrayModel(field.of as Cls);
|
|
17
|
+
return { key: arraiedModel(getValueExample(valueRef as Cls), valueArrDepth) };
|
|
18
|
+
};
|
|
19
|
+
|
|
15
20
|
const getResponseExample = (ref: Cls | Cls[]) => {
|
|
16
21
|
const [modelRef, arrDepth] = getNonArrayModel(ref);
|
|
17
22
|
const isPrimitive = PrimitiveRegistry.has(modelRef);
|
|
@@ -21,6 +26,7 @@ const getResponseExample = (ref: Cls | Cls[]) => {
|
|
|
21
26
|
Object.entries((modelRef as ConstantCls)[FIELD_META]).forEach(([key, field]) => {
|
|
22
27
|
if (field.example) example[key] = field.example as unknown;
|
|
23
28
|
else if (field.enum) example[key] = arraiedModel<string>(field.enum.values[0] as string, field.arrDepth);
|
|
29
|
+
else if (field.isMap) example[key] = getMapExample(field, getResponseExample);
|
|
24
30
|
else example[key] = getResponseExample(field.modelRef);
|
|
25
31
|
});
|
|
26
32
|
const result = arraiedModel(example, arrDepth);
|
|
@@ -33,7 +39,8 @@ const getRequestExample = (modelRef: Cls) => {
|
|
|
33
39
|
if (isPrimitive) return (modelRef as typeof PrimitiveScalar)[EXAMPLE_VALUE];
|
|
34
40
|
else {
|
|
35
41
|
Object.entries((modelRef as ConstantCls)[FIELD_META]).forEach(([key, field]) => {
|
|
36
|
-
if (
|
|
42
|
+
if (field.isMap) example[key] = getMapExample(field, getRequestExample);
|
|
43
|
+
else if (!field.isScalar && field.isClass) example[key] = "ObjectID";
|
|
37
44
|
else
|
|
38
45
|
example[key] = (
|
|
39
46
|
(field.example ?? field.enum)
|
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/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;
|
|
@@ -313,22 +313,30 @@ export class AgentSession {
|
|
|
313
313
|
* Restoring follows the rule an async `load` already follows: it lands only while nothing has happened to this
|
|
314
314
|
* session yet. Attach before the first turn and it restores; attach after and it saves from there on, with the
|
|
315
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.
|
|
316
318
|
*/
|
|
317
319
|
setHistory = (history: SessionHistory | null) => {
|
|
318
320
|
this.#history = history ?? undefined;
|
|
319
|
-
if (
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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);
|
|
324
327
|
}
|
|
325
|
-
|
|
326
|
-
|
|
328
|
+
|
|
329
|
+
return () => {
|
|
330
|
+
if (history && this.#history === history) this.#history = undefined;
|
|
331
|
+
};
|
|
327
332
|
};
|
|
328
333
|
|
|
329
334
|
/** The compaction hook as a setter, for the same reason `setHistory` is one: a host attaches it after the fact. */
|
|
330
335
|
setOnCompact = (onCompact: AgentSessionOptions["onCompact"] | null) => {
|
|
331
336
|
this.#onCompact = onCompact ?? undefined;
|
|
337
|
+
return () => {
|
|
338
|
+
if (onCompact && this.#onCompact === onCompact) this.#onCompact = undefined;
|
|
339
|
+
};
|
|
332
340
|
};
|
|
333
341
|
|
|
334
342
|
/**
|