akanjs 3.0.0-alpha.11 → 3.0.0-alpha.13

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.
Files changed (69) hide show
  1. package/base/symbols.ts +4 -0
  2. package/constant/index.ts +1 -0
  3. package/constant/mask.ts +60 -0
  4. package/dictionary/dictInfo.ts +12 -1
  5. package/fetch/client/fetchClient.ts +9 -0
  6. package/package.json +1 -1
  7. package/server/akanApp.ts +3 -1
  8. package/service/predefinedAdaptor/index.ts +1 -0
  9. package/service/predefinedAdaptor/insightQuery.ts +183 -0
  10. package/signal/mcp/Msg.ts +5 -33
  11. package/store/action.ts +9 -20
  12. package/store/actionTag.ts +28 -0
  13. package/store/agent/AgentBridge.ts +281 -0
  14. package/store/agent/StoreCatalogue.ts +296 -0
  15. package/store/agent/index.ts +3 -0
  16. package/store/agent/types.ts +50 -0
  17. package/store/databaseStateNames.ts +31 -0
  18. package/store/formSetterNames.ts +21 -0
  19. package/store/index.ts +7 -0
  20. package/store/rootStore.ts +2 -1
  21. package/store/sliceRole.ts +36 -0
  22. package/store/state.ts +2 -12
  23. package/store/store.ts +5 -0
  24. package/store/storeInstance.ts +54 -16
  25. package/store/storeRegistry.ts +10 -0
  26. package/types/base/symbols.d.ts +4 -0
  27. package/types/constant/index.d.ts +1 -0
  28. package/types/constant/mask.d.ts +34 -0
  29. package/types/dictionary/dictInfo.d.ts +9 -2
  30. package/types/fetch/client/fetchClient.d.ts +9 -0
  31. package/types/service/predefinedAdaptor/index.d.ts +1 -0
  32. package/types/service/predefinedAdaptor/insightQuery.d.ts +50 -0
  33. package/types/signal/mcp/Msg.d.ts +3 -4
  34. package/types/store/actionTag.d.ts +17 -0
  35. package/types/store/agent/AgentBridge.d.ts +70 -0
  36. package/types/store/agent/StoreCatalogue.d.ts +21 -0
  37. package/types/store/agent/index.d.ts +3 -0
  38. package/types/store/agent/types.d.ts +49 -0
  39. package/types/store/agent.d.ts +1 -0
  40. package/types/store/databaseStateNames.d.ts +25 -0
  41. package/types/store/formSetterNames.d.ts +16 -0
  42. package/types/store/index.d.ts +6 -0
  43. package/types/store/rootStore.d.ts +4 -1
  44. package/types/store/sliceRole.d.ts +25 -0
  45. package/types/store/store.d.ts +4 -1
  46. package/types/store/storeInstance.d.ts +16 -0
  47. package/types/store/storeRegistry.d.ts +3 -0
  48. package/types/ui/Agent/Dock.d.ts +16 -0
  49. package/types/ui/Agent/Section.d.ts +10 -0
  50. package/types/ui/Agent/StateKey.d.ts +15 -0
  51. package/types/ui/Agent/Tool.d.ts +15 -0
  52. package/types/ui/Agent/Transcript.d.ts +13 -0
  53. package/types/ui/Agent/index.d.ts +11 -0
  54. package/types/ui/Agent.d.ts +1 -0
  55. package/types/ui/agentAttrs.d.ts +14 -0
  56. package/types/ui/index.d.ts +2 -0
  57. package/ui/Agent/Dock.tsx +61 -0
  58. package/ui/Agent/Section.tsx +24 -0
  59. package/ui/Agent/StateKey.tsx +42 -0
  60. package/ui/Agent/Tool.tsx +66 -0
  61. package/ui/Agent/Transcript.tsx +33 -0
  62. package/ui/Agent/index.ts +7 -0
  63. package/ui/Button.tsx +2 -0
  64. package/ui/Field.tsx +15 -7
  65. package/ui/Input.tsx +7 -0
  66. package/ui/Select.tsx +2 -1
  67. package/ui/Switch.tsx +2 -0
  68. package/ui/agentAttrs.ts +19 -0
  69. package/ui/index.ts +2 -0
@@ -0,0 +1,49 @@
1
+ import type { SerializedArg } from "akanjs/signal";
2
+ import type { SliceActionKey } from "../sliceRole.d.ts";
3
+ import type { SliceStateKey } from "../state.d.ts";
4
+ /** Which generated form-setter a store key is, when it is one. */
5
+ export type FormSetterRole = "set" | "add" | "sub" | "addOrSub";
6
+ /** What calling a store action reaches. `state` never leaves the browser. */
7
+ export type StoreActionEffect = "state" | "query" | "mutation";
8
+ export interface SerializedStoreAction {
9
+ args: SerializedArg[];
10
+ effect: StoreActionEffect;
11
+ /** The model this key belongs to, when the key identifies one. */
12
+ refName?: string;
13
+ /** The endpoint whose arguments it borrows, when it is named after one. */
14
+ endpoint?: string;
15
+ /** The generated role, absent on an action a module wrote itself. */
16
+ role?: SliceActionKey | FormSetterRole;
17
+ /** The model field it writes, on a form setter. */
18
+ field?: string;
19
+ }
20
+ export interface SerializedStoreState {
21
+ /**
22
+ * What the live value is. A store declares no types — `STATE_META` holds initial values — so this is read off the
23
+ * value the store is holding, and a key initialized to `null` or `[]` says nothing about what may go into it.
24
+ */
25
+ type: "string" | "number" | "boolean" | "date" | "list" | "map" | "object" | "unknown";
26
+ /** The model this key holds, when it holds one. What a read of it is masked by. */
27
+ refName?: string;
28
+ /** Which of the model's five classes, which is what decides the fields a read may carry. */
29
+ modelType?: "input" | "full" | "light" | "insight";
30
+ /** Materialized from a computation, the URL, or storage. Writing one throws, so it is read-only by construction. */
31
+ derived: boolean;
32
+ role?: SliceStateKey;
33
+ }
34
+ /**
35
+ * One store instance described for an agent: every key it can read and every action it may call.
36
+ *
37
+ * Flat rather than grouped by model, because that is what the store is — `st.use.x` and `st.do.y` are one namespace,
38
+ * so a key means the same thing to every reader and two models cannot both claim one. It is the store's answer to
39
+ * `SerializedSignal`, and it is derived on the client from the built store rather than shipped from the server: the
40
+ * store classes are in the bundle already, and a second copy over the wire is a second thing to keep in step.
41
+ */
42
+ export interface SerializedStore {
43
+ state: {
44
+ [key: string]: SerializedStoreState;
45
+ };
46
+ action: {
47
+ [key: string]: SerializedStoreAction;
48
+ };
49
+ }
@@ -0,0 +1 @@
1
+ export * from "./agent/index.d.ts";
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The state keys every model store holds, and which of the model's five classes each one is an instance of.
3
+ *
4
+ * Shared with the agent catalogue, which needs the class rather than the key: a value read out of the store is
5
+ * masked by the model it belongs to, and `immerify` drops the constructor, so the live value cannot say what it is.
6
+ * `modelForm` is the one that matters most — it is an `Input` holding whatever the user has typed, credentials
7
+ * included, and it reaches the catalogue as a plain object.
8
+ */
9
+ export declare const databaseStateNames: (refName: string) => {
10
+ model: string;
11
+ modelLoading: string;
12
+ modelForm: string;
13
+ modelFormLoading: string;
14
+ modelSubmit: string;
15
+ modelViewAt: string;
16
+ modelModal: string;
17
+ modelOperation: string;
18
+ defaultModel: string;
19
+ };
20
+ /** Which of the model's classes each generated state key holds, for the keys that hold one at all. */
21
+ export declare const databaseStateModelTypes: {
22
+ readonly model: "full";
23
+ readonly modelForm: "input";
24
+ readonly defaultModel: "full";
25
+ };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The keys `makeFormSetter` publishes for one field of one model.
3
+ *
4
+ * Shared with the agent catalogue, which has to answer what `setNameOnUser` takes without parsing the name apart —
5
+ * `set(.+)On(.+)` has more than one reading whenever a field or a model name contains `On`. Computing the same names
6
+ * forward from the same field metadata is unambiguous, and keeps the two from drifting when a name changes here.
7
+ */
8
+ export declare const formSetterNames: (className: string, key: string) => {
9
+ field: string;
10
+ Field: string;
11
+ setFieldOnModel: string;
12
+ addFieldOnModel: string;
13
+ subFieldOnModel: string;
14
+ addOrSubFieldOnModel: string;
15
+ uploadFieldOnModel: string;
16
+ };
@@ -1,8 +1,14 @@
1
1
  export * from "./action.d.ts";
2
+ export * from "./actionTag.d.ts";
3
+ export * from "./agent.d.ts";
2
4
  export * from "./baseSt.d.ts";
5
+ export * from "./databaseStateNames.d.ts";
6
+ export * from "./formSetterNames.d.ts";
3
7
  export * from "./rootStore.d.ts";
8
+ export * from "./sliceRole.d.ts";
4
9
  export * from "./state.d.ts";
5
10
  export * from "./stateBuilder.d.ts";
6
11
  export * from "./store.d.ts";
12
+ export { StoreInstance } from "./storeInstance.d.ts";
7
13
  export * from "./storeRegistry.d.ts";
8
14
  export * from "./types.d.ts";
@@ -1,4 +1,4 @@
1
- import type { ACTION_META, Cls, STATE_DERIVED_META, STATE_INIT_META, STATE_META } from "akanjs/base";
1
+ import type { ACTION_META, ACTION_OWNER_META, Cls, STATE_DERIVED_META, STATE_INIT_META, STATE_META } from "akanjs/base";
2
2
  import type { SerializedSlice } from "akanjs/signal";
3
3
  import type { StateDerivedMeta, StateInitializerMap } from "./stateBuilder.d.ts";
4
4
  import type { SetGetWritable } from "./types.d.ts";
@@ -13,6 +13,9 @@ export type RootStoreCls<RefName extends string = any, WritableState = any, Acti
13
13
  [ACTION_META]: {
14
14
  [key: string]: (...args: any[]) => any;
15
15
  };
16
+ [ACTION_OWNER_META]: {
17
+ [key: string]: string;
18
+ };
16
19
  slice: {
17
20
  [key: string]: {
18
21
  [key: string]: SerializedSlice;
@@ -0,0 +1,25 @@
1
+ import type { SerializedArg } from "akanjs/signal";
2
+ import type { SliceStateKey } from "./state.d.ts";
3
+ /** The generated slice actions, named by what they do rather than by the key any one slice publishes them under. */
4
+ export type SliceActionKey = "initModel" | "refreshModel" | "selectModel" | "setPageOfModel" | "addPageOfModel" | "setLimitOfModel" | "setQueryArgsOfModel" | "setSortOfModel";
5
+ /**
6
+ * What a generated key on `st.do` / `st.use` actually is.
7
+ *
8
+ * A slice publishes its keys with the model and the suffix spliced into the role name — `setPageOfUserInOrg` for the
9
+ * `setPageOfModel` role of `user`'s `inOrg` slice — and the splicing is string replacement over capitalized names.
10
+ * Recording the role while the key is being built is what saves a reader of the finished store from running that
11
+ * replacement backwards, which does not have one answer: on a model named `page`, `setPageOfPage` is both the
12
+ * paging action and a field setter.
13
+ */
14
+ export interface SliceActionRole {
15
+ role: SliceActionKey;
16
+ refName: string;
17
+ sliceName: string;
18
+ /** The slice's own arguments, which the `initModel` and `setQueryArgsOfModel` roles take positionally. */
19
+ args: SerializedArg[];
20
+ }
21
+ export interface SliceStateRole {
22
+ role: SliceStateKey;
23
+ refName: string;
24
+ sliceName: string;
25
+ }
@@ -1,4 +1,4 @@
1
- import { ACTION_META, type Cls, type MergeAllActionTypes, type MergeAllKeyOfObjects, type SLICE_META, STATE_DERIVED_META, STATE_INIT_META, STATE_META } from "akanjs/base";
1
+ import { ACTION_META, ACTION_OWNER_META, type Cls, type MergeAllActionTypes, type MergeAllKeyOfObjects, type SLICE_META, STATE_DERIVED_META, STATE_INIT_META, STATE_META } from "akanjs/base";
2
2
  import type { FilterInstance } from "akanjs/document";
3
3
  import type { ClientSignal } from "akanjs/fetch";
4
4
  import type { SerializedSlice, SlceCnstCapitalizedRefName, SlceCnstDefault, SlceCnstFull, SlceCnstInput, SlceCnstInsight, SlceCnstLight, SlceDbFilter, SlceDbSort, SliceCls } from "akanjs/signal";
@@ -33,6 +33,9 @@ export type StoreCls<RefName extends string = string, WritableState = any, Actio
33
33
  [ACTION_META]: {
34
34
  [key: string]: (...args: any[]) => any;
35
35
  };
36
+ [ACTION_OWNER_META]: {
37
+ [key: string]: string;
38
+ };
36
39
  slice: {
37
40
  [key: string]: SerializedSlice;
38
41
  };
@@ -1,5 +1,6 @@
1
1
  import type { RefObject } from "react";
2
2
  import type { RootStoreCls } from "./rootStore.d.ts";
3
+ import type { SliceActionRole, SliceStateRole } from "./sliceRole.d.ts";
3
4
  type StoreStateRecord = Record<string, unknown>;
4
5
  type StoreAction = (...args: unknown[]) => unknown;
5
6
  export type ReactAPI = {
@@ -32,6 +33,21 @@ export declare class StoreInstance {
32
33
  slice: {
33
34
  [key: string]: unknown;
34
35
  };
36
+ /** Which module declared each action. The dictionary node its words are written in is named after it. */
37
+ get actionOwners(): ReadonlyMap<string, string>;
38
+ /**
39
+ * How many arguments each action declares.
40
+ *
41
+ * Recorded because `do[key]` is a rest-argument wrapper around the real method, so its own `length` is zero for
42
+ * everything — the arity is gone by the time anyone holding the instance could ask. A rest parameter on the method
43
+ * itself still reads as zero; nothing can recover that.
44
+ */
45
+ get actionArity(): ReadonlyMap<string, number>;
46
+ /** What each generated slice key is, for a reader that has only the finished store. See `SliceActionRole`. */
47
+ get sliceActionRoles(): ReadonlyMap<string, SliceActionRole>;
48
+ get sliceStateRoles(): ReadonlyMap<string, SliceStateRole>;
49
+ /** Keys the store materializes from a computation, the URL, or storage. `set` throws on them. */
50
+ get derivedKeys(): ReadonlySet<string>;
35
51
  constructor(store?: RootStoreCls);
36
52
  addStore(store: RootStoreCls): this;
37
53
  }
@@ -1,11 +1,14 @@
1
1
  import { type MergeAllKeyOfObjects, type MergeAllKeyOfTypes, type MergeAllTypes, STATE_META } from "akanjs/base";
2
2
  import type { RootStoreCls } from "./rootStore.d.ts";
3
3
  import type { StoreCls } from "./store.d.ts";
4
+ import { StoreInstance } from "./storeInstance.d.ts";
4
5
  export type { ReactAPI } from "./storeInstance.d.ts";
5
6
  import type { SetGet } from "./types.d.ts";
6
7
  import type { WithSelectors } from "./withSelector.d.ts";
7
8
  export declare class StoreRegistry {
8
9
  #private;
10
+ /** The one store every `st.use` / `st.do` in the process goes through. What an agent bridge drives. */
11
+ static get instance(): StoreInstance;
9
12
  static register<StrCls extends StoreCls>(store: StrCls): StrCls;
10
13
  static get(refName: string): StoreCls | undefined;
11
14
  static merge<RefName extends string, StoreClses extends (StoreCls | RootStoreCls)[]>(refName: RefName, ...stores: StoreClses): RootStoreCls<RefName, MergeAllKeyOfObjects<StoreClses, typeof STATE_META>, MergeAllTypes<StoreClses, keyof SetGet | "slice">, MergeAllKeyOfTypes<StoreClses, "slice">, MergeAllKeyOfObjects<StoreClses, "_slice">>;
@@ -0,0 +1,16 @@
1
+ import { AgentBridge } from "akanjs/store";
2
+ export interface DockProps {
3
+ className?: string;
4
+ /** Pass the bridge an agent is already driving so both write into one transcript. Defaults to the app's own. */
5
+ bridge?: AgentBridge;
6
+ open?: boolean;
7
+ }
8
+ /**
9
+ * The in-page surface of the agent bridge: what this page lets an agent do, what it refused, and what it has done.
10
+ *
11
+ * It drives the store directly rather than talking to a model, because a model is not the framework's to choose —
12
+ * there is no provider or key here. An app wires its own agent to `bridge.tools` / `bridge.call` and can render this
13
+ * beside it; on its own it is the way to see the catalogue a page actually publishes, which is the one thing no
14
+ * amount of reading the source answers.
15
+ */
16
+ export declare const Dock: ({ className, bridge, open }: DockProps) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,10 @@
1
+ import type { ReactNode } from "react";
2
+ interface SectionProps {
3
+ className?: string;
4
+ title: string;
5
+ count: number;
6
+ children: ReactNode;
7
+ open?: boolean;
8
+ }
9
+ export default function Section({ className, title, count, children, open }: SectionProps): import("react/jsx-runtime").JSX.Element;
10
+ export {};
@@ -0,0 +1,15 @@
1
+ import type { AgentBridge, SerializedStoreState } from "akanjs/store";
2
+ interface StateKeyProps {
3
+ className?: string;
4
+ bridge: AgentBridge;
5
+ name: string;
6
+ entry: SerializedStoreState;
7
+ }
8
+ /**
9
+ * One readable state key, read on demand rather than rendered with the rest.
10
+ *
11
+ * Reading is where masking happens, so a key that holds an object no model claims refuses here instead of in the
12
+ * catalogue — whether it is readable depends on what is in it, which the catalogue cannot know ahead of time.
13
+ */
14
+ export default function StateKey({ className, bridge, name, entry }: StateKeyProps): import("react/jsx-runtime").JSX.Element;
15
+ export {};
@@ -0,0 +1,15 @@
1
+ import type { AgentBridge, AgentTool } from "akanjs/store";
2
+ interface ToolProps {
3
+ className?: string;
4
+ bridge: AgentBridge;
5
+ tool: AgentTool;
6
+ onRun: () => void;
7
+ }
8
+ /**
9
+ * One published action, with the arguments as JSON rather than as a generated form.
10
+ *
11
+ * A form per argument is what the API explorer does, and it is the wrong trade here: the point of the dock is to
12
+ * watch a call land in the running app, and every schema shape the store publishes is already legible as JSON.
13
+ */
14
+ export default function Tool({ className, bridge, tool, onRun }: ToolProps): import("react/jsx-runtime").JSX.Element;
15
+ export {};
@@ -0,0 +1,13 @@
1
+ import type { AgentCall } from "akanjs/store";
2
+ interface TranscriptProps {
3
+ className?: string;
4
+ calls: readonly AgentCall[];
5
+ }
6
+ /**
7
+ * What the agent did, newest last, so the user can check it against what they saw the page do.
8
+ *
9
+ * There is no undo here. Every model removal in this framework is soft, so one is possible in principle, but which
10
+ * writes are reversible is a per-module judgement and `local/agent-native/PLAN.md` §9-3 leaves it open.
11
+ */
12
+ export default function Transcript({ className, calls }: TranscriptProps): import("react/jsx-runtime").JSX.Element;
13
+ export {};
@@ -0,0 +1,11 @@
1
+ import Section from "./Section.d.ts";
2
+ import StateKey from "./StateKey.d.ts";
3
+ import Tool from "./Tool.d.ts";
4
+ import Transcript from "./Transcript.d.ts";
5
+ export declare const Agent: {
6
+ Dock: ({ className, bridge, open }: import("./Dock.d.ts").DockProps) => import("react/jsx-runtime").JSX.Element;
7
+ Section: typeof Section;
8
+ StateKey: typeof StateKey;
9
+ Tool: typeof Tool;
10
+ Transcript: typeof Transcript;
11
+ };
@@ -0,0 +1 @@
1
+ export * from "./Agent/index.d.ts";
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The `data-akan-*` attributes for a handler the caller passed by reference.
3
+ *
4
+ * `{}` when the handler is an inline arrow, which is the honest answer: a closure the caller wrote says nothing
5
+ * about what it does, and a guessed annotation is worse than none. The house form for a model field is the setter
6
+ * itself (`onChange={st.do.setNameOnUser}`), so the fields that matter are annotated without an app writing anything.
7
+ *
8
+ * What it buys, beyond an in-page agent: an accessibility tree and E2E selectors that name the action rather than a
9
+ * class, and an external browser agent — one this framework has no bridge into — reading the same names.
10
+ */
11
+ export declare const agentAttrs: (handler: unknown) => {
12
+ "data-akan-action"?: string;
13
+ "data-akan-state"?: string;
14
+ } | Record<string, never>;
@@ -1,3 +1,5 @@
1
+ export { Agent } from "./Agent.d.ts";
2
+ export { agentAttrs } from "./agentAttrs.d.ts";
1
3
  export { animated } from "./animated.d.ts";
2
4
  export { Badge } from "./Badge.d.ts";
3
5
  export { BottomSheet, type BottomSheetRef } from "./BottomSheet.d.ts";
@@ -0,0 +1,61 @@
1
+ "use client";
2
+ import { cn } from "akanjs/client";
3
+ import { AgentBridge } from "akanjs/store";
4
+ import { useRef, useState } from "react";
5
+ import Section from "./Section";
6
+ import StateKey from "./StateKey";
7
+ import Tool from "./Tool";
8
+ import Transcript from "./Transcript";
9
+
10
+ export interface DockProps {
11
+ className?: string;
12
+ /** Pass the bridge an agent is already driving so both write into one transcript. Defaults to the app's own. */
13
+ bridge?: AgentBridge;
14
+ open?: boolean;
15
+ }
16
+
17
+ /**
18
+ * The in-page surface of the agent bridge: what this page lets an agent do, what it refused, and what it has done.
19
+ *
20
+ * It drives the store directly rather than talking to a model, because a model is not the framework's to choose —
21
+ * there is no provider or key here. An app wires its own agent to `bridge.tools` / `bridge.call` and can render this
22
+ * beside it; on its own it is the way to see the catalogue a page actually publishes, which is the one thing no
23
+ * amount of reading the source answers.
24
+ */
25
+ export const Dock = ({ className, bridge, open = false }: DockProps) => {
26
+ const held = useRef<AgentBridge | null>(null);
27
+ held.current ??= bridge ?? AgentBridge.of();
28
+ const agent = held.current;
29
+ const [ran, setRan] = useState(0);
30
+ return (
31
+ <aside
32
+ className={cn(
33
+ "fixed right-4 bottom-4 z-50 flex max-h-[70vh] w-80 flex-col gap-2 overflow-y-auto rounded-box border border-base-content/10 bg-base-100/95 p-3 shadow-lg",
34
+ className,
35
+ )}
36
+ >
37
+ <h2 className="font-semibold text-sm">Agent</h2>
38
+ <Section count={agent.tools.length} open={open} title="Actions">
39
+ {agent.tools.map((tool) => (
40
+ <Tool bridge={agent} key={tool.name} onRun={() => setRan(ran + 1)} tool={tool} />
41
+ ))}
42
+ </Section>
43
+ <Section count={Object.keys(agent.state).length} title="State">
44
+ {Object.entries(agent.state).map(([name, entry]) => (
45
+ <StateKey bridge={agent} entry={entry} key={name} name={name} />
46
+ ))}
47
+ </Section>
48
+ <Section count={agent.refusals.length} title="Refused">
49
+ {agent.refusals.map((refusal) => (
50
+ <div className="flex flex-col" key={refusal.key}>
51
+ <span className="truncate font-mono text-xs">{refusal.key}</span>
52
+ <span className="text-[10px] text-foreground/50">{refusal.reason}</span>
53
+ </div>
54
+ ))}
55
+ </Section>
56
+ <Section count={agent.transcript.length} open title="Transcript">
57
+ <Transcript calls={agent.transcript} />
58
+ </Section>
59
+ </aside>
60
+ );
61
+ };
@@ -0,0 +1,24 @@
1
+ "use client";
2
+ import { cn } from "akanjs/client";
3
+ import type { ReactNode } from "react";
4
+
5
+ interface SectionProps {
6
+ className?: string;
7
+ title: string;
8
+ count: number;
9
+ children: ReactNode;
10
+ open?: boolean;
11
+ }
12
+
13
+ export default function Section({ className, title, count, children, open }: SectionProps) {
14
+ return (
15
+ <details className={cn("group rounded-box bg-muted", className)} open={open}>
16
+ <summary className="flex cursor-pointer list-none items-center gap-2 px-3 py-2 [&::-webkit-details-marker]:hidden">
17
+ <span className="font-semibold text-sm">{title}</span>
18
+ <span className="text-foreground/50 text-xs">{count}</span>
19
+ <span className="ml-auto text-foreground/40 transition-transform group-open:rotate-180">▾</span>
20
+ </summary>
21
+ <div className="flex flex-col gap-1 px-3 pb-3">{children}</div>
22
+ </details>
23
+ );
24
+ }
@@ -0,0 +1,42 @@
1
+ "use client";
2
+ import { cn } from "akanjs/client";
3
+ import type { AgentBridge, SerializedStoreState } from "akanjs/store";
4
+ import { useState } from "react";
5
+
6
+ interface StateKeyProps {
7
+ className?: string;
8
+ bridge: AgentBridge;
9
+ name: string;
10
+ entry: SerializedStoreState;
11
+ }
12
+
13
+ /**
14
+ * One readable state key, read on demand rather than rendered with the rest.
15
+ *
16
+ * Reading is where masking happens, so a key that holds an object no model claims refuses here instead of in the
17
+ * catalogue — whether it is readable depends on what is in it, which the catalogue cannot know ahead of time.
18
+ */
19
+ export default function StateKey({ className, bridge, name, entry }: StateKeyProps) {
20
+ const [shown, setShown] = useState("");
21
+ const read = () => {
22
+ try {
23
+ setShown(JSON.stringify(bridge.read(name), null, 2));
24
+ } catch (thrown) {
25
+ setShown(thrown instanceof Error ? thrown.message : String(thrown));
26
+ }
27
+ };
28
+ return (
29
+ <div className={cn("flex flex-col gap-1", className)}>
30
+ <button className="flex items-center gap-2 text-left" type="button" onClick={read}>
31
+ <span className="truncate font-mono text-xs">{name}</span>
32
+ <span className="ml-auto shrink-0 text-[10px] text-foreground/40">
33
+ {entry.refName ? `${entry.refName}.${entry.modelType ?? ""}` : entry.type}
34
+ {entry.derived ? " · derived" : ""}
35
+ </span>
36
+ </button>
37
+ {shown ? (
38
+ <pre className="max-h-40 overflow-auto rounded-field bg-muted p-2 text-[10px] leading-tight">{shown}</pre>
39
+ ) : null}
40
+ </div>
41
+ );
42
+ }
@@ -0,0 +1,66 @@
1
+ "use client";
2
+ import { cn } from "akanjs/client";
3
+ import type { AgentBridge, AgentTool } from "akanjs/store";
4
+ import { useState } from "react";
5
+ import { buttonRecipe } from "../recipe";
6
+
7
+ const effectClass: { [key in AgentTool["effect"]]: string } = {
8
+ state: "bg-muted text-foreground/70",
9
+ query: "bg-info/15 text-info",
10
+ mutation: "bg-warning/15 text-warning",
11
+ };
12
+
13
+ interface ToolProps {
14
+ className?: string;
15
+ bridge: AgentBridge;
16
+ tool: AgentTool;
17
+ onRun: () => void;
18
+ }
19
+
20
+ /**
21
+ * One published action, with the arguments as JSON rather than as a generated form.
22
+ *
23
+ * A form per argument is what the API explorer does, and it is the wrong trade here: the point of the dock is to
24
+ * watch a call land in the running app, and every schema shape the store publishes is already legible as JSON.
25
+ */
26
+ export default function Tool({ className, bridge, tool, onRun }: ToolProps) {
27
+ const [args, setArgs] = useState("{}");
28
+ const [error, setError] = useState("");
29
+ const run = async () => {
30
+ setError("");
31
+ try {
32
+ await bridge.call(tool.name, JSON.parse(args) as Record<string, unknown>);
33
+ } catch (thrown) {
34
+ setError(thrown instanceof Error ? thrown.message : String(thrown));
35
+ }
36
+ onRun();
37
+ };
38
+ return (
39
+ <details className={cn("rounded-field bg-base-100/60 px-2 py-1", className)}>
40
+ <summary className="flex cursor-pointer list-none items-center gap-2 [&::-webkit-details-marker]:hidden">
41
+ <span className={cn("rounded-field px-1.5 py-0.5 text-[10px] uppercase", effectClass[tool.effect])}>
42
+ {tool.effect}
43
+ </span>
44
+ <span className="truncate font-mono text-xs">{tool.name}</span>
45
+ </summary>
46
+ <div className="flex flex-col gap-2 py-2">
47
+ {tool.description ? <p className="text-foreground/70 text-xs">{tool.description}</p> : null}
48
+ <pre className="overflow-x-auto rounded-field bg-muted p-2 text-[10px] leading-tight">
49
+ {JSON.stringify(tool.inputSchema.properties, null, 2)}
50
+ </pre>
51
+ <textarea
52
+ className="w-full rounded-field bg-muted p-2 font-mono text-xs"
53
+ rows={2}
54
+ value={args}
55
+ onChange={(event) => setArgs(event.target.value)}
56
+ />
57
+ {/* `buttonRecipe` rather than `Button`, which reads the app runtime through `usePage()` for labels this
58
+ developer surface does not localize anyway — the same English the API explorer uses. */}
59
+ <button className={buttonRecipe({ size: "xs" })} onClick={run} type="button">
60
+ Run
61
+ </button>
62
+ {error ? <p className="text-error text-xs">{error}</p> : null}
63
+ </div>
64
+ </details>
65
+ );
66
+ }
@@ -0,0 +1,33 @@
1
+ "use client";
2
+ import { cn } from "akanjs/client";
3
+ import type { AgentCall } from "akanjs/store";
4
+
5
+ interface TranscriptProps {
6
+ className?: string;
7
+ calls: readonly AgentCall[];
8
+ }
9
+
10
+ /**
11
+ * What the agent did, newest last, so the user can check it against what they saw the page do.
12
+ *
13
+ * There is no undo here. Every model removal in this framework is soft, so one is possible in principle, but which
14
+ * writes are reversible is a per-module judgement and `local/agent-native/PLAN.md` §9-3 leaves it open.
15
+ */
16
+ export default function Transcript({ className, calls }: TranscriptProps) {
17
+ return (
18
+ <div className={cn("flex flex-col gap-1", className)}>
19
+ {calls.map((call, idx) => (
20
+ <div className="flex flex-col rounded-field bg-base-100/60 px-2 py-1" key={idx}>
21
+ <div className="flex items-baseline gap-2">
22
+ <span className="truncate font-mono text-xs">{call.name}</span>
23
+ <span className="ml-auto shrink-0 text-[10px] text-foreground/40">
24
+ {call.at.toISOString().slice(11, 19)}
25
+ </span>
26
+ </div>
27
+ <span className="truncate font-mono text-[10px] text-foreground/50">{JSON.stringify(call.args)}</span>
28
+ {call.error ? <span className="text-[10px] text-error">{call.error}</span> : null}
29
+ </div>
30
+ ))}
31
+ </div>
32
+ );
33
+ }
@@ -0,0 +1,7 @@
1
+ import { Dock } from "./Dock";
2
+ import Section from "./Section";
3
+ import StateKey from "./StateKey";
4
+ import Tool from "./Tool";
5
+ import Transcript from "./Transcript";
6
+
7
+ export const Agent = { Dock, Section, StateKey, Tool, Transcript };
package/ui/Button.tsx CHANGED
@@ -4,6 +4,7 @@ import { isThenable } from "akanjs/common";
4
4
  import type React from "react";
5
5
  import { type ButtonHTMLAttributes, type ComponentType, createElement, useState } from "react";
6
6
  import { AiFillCheckCircle, AiOutlineLoading3Quarters } from "react-icons/ai";
7
+ import { agentAttrs } from "./agentAttrs";
7
8
  import { type ButtonVariants, buttonRecipe } from "./recipe";
8
9
  import { useUiOverride, useUiRecipe } from "./UiOverride";
9
10
 
@@ -91,6 +92,7 @@ const DefaultButton = <Result = unknown>({
91
92
  cn(loadingMode === "hold" && "relative", busy && !rest.disabled && "disabled:opacity-100", className),
92
93
  )}
93
94
  {...rest}
95
+ {...agentAttrs(onClick)}
94
96
  disabled={!!rest.disabled || busy}
95
97
  onClick={(e) => {
96
98
  if (!onClick) return;