akanjs 3.0.0-alpha.24 → 3.0.0-alpha.26

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 (73) hide show
  1. package/client/clientRuntime.ts +2 -0
  2. package/dictionary/base.dictionary.ts +5 -0
  3. package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
  4. package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
  5. package/package.json +1 -1
  6. package/signal/serializer/fetch.serializer.ts +4 -0
  7. package/store/agent/AgentBridge.ts +30 -266
  8. package/store/agent/AgentContext.ts +3 -6
  9. package/store/agent/StoreCatalogue.ts +16 -251
  10. package/store/agent/StoreSurfaceSource.ts +8 -35
  11. package/store/agent/index.ts +0 -1
  12. package/store/agent/types.ts +0 -34
  13. package/store/agentic/StToolBuilder.ts +4 -3
  14. package/store/baseSt.ts +0 -7
  15. package/store/formSetterNames.ts +3 -3
  16. package/store/store.ts +1 -10
  17. package/store/types.ts +1 -7
  18. package/types/client/clientRuntime.d.ts +2 -0
  19. package/types/dictionary/base.dictionary.d.ts +1 -1
  20. package/types/dictionary/dictionary.d.ts +8 -8
  21. package/types/store/agent/AgentBridge.d.ts +19 -59
  22. package/types/store/agent/AgentContext.d.ts +2 -2
  23. package/types/store/agent/StoreCatalogue.d.ts +16 -16
  24. package/types/store/agent/StoreSurfaceSource.d.ts +6 -7
  25. package/types/store/agent/index.d.ts +0 -1
  26. package/types/store/agent/types.d.ts +0 -34
  27. package/types/store/baseSt.d.ts +0 -7
  28. package/types/store/formSetterNames.d.ts +3 -3
  29. package/types/store/store.d.ts +1 -2
  30. package/types/store/types.d.ts +1 -8
  31. package/types/ui/Agent/Dock.d.ts +9 -8
  32. package/types/ui/Agent/Tool.d.ts +6 -6
  33. package/types/ui/Agent/Transcript.d.ts +1 -1
  34. package/types/ui/Agent/index.d.ts +1 -1
  35. package/types/ui/Data/Dashboard.d.ts +3 -2
  36. package/types/ui/Data/Insight.d.ts +1 -1
  37. package/types/ui/Data/ListContainer.d.ts +3 -3
  38. package/types/ui/Data/dataExport.d.ts +5 -0
  39. package/types/ui/Data/dataText.d.ts +6 -0
  40. package/types/ui/Model/AdminPanel.d.ts +6 -2
  41. package/types/vendor/use-agentic/AgenticSurface.d.ts +3 -1
  42. package/types/vendor/use-agentic/types.d.ts +7 -0
  43. package/ui/Agent/Dock.tsx +20 -17
  44. package/ui/Agent/Tool.tsx +18 -11
  45. package/ui/Agent/Transcript.tsx +1 -1
  46. package/ui/BottomSheet.tsx +1 -1
  47. package/ui/Data/CardList.tsx +14 -3
  48. package/ui/Data/Dashboard.tsx +34 -50
  49. package/ui/Data/Insight.tsx +18 -12
  50. package/ui/Data/Item.tsx +42 -56
  51. package/ui/Data/ListContainer.tsx +116 -116
  52. package/ui/Data/TableList.tsx +22 -13
  53. package/ui/Data/dataExport.ts +54 -0
  54. package/ui/Data/dataText.ts +22 -0
  55. package/ui/KeyboardAvoiding.tsx +2 -2
  56. package/ui/Layout/Sider.tsx +1 -1
  57. package/ui/Link/CsrLink.tsx +1 -1
  58. package/ui/Menu.tsx +1 -1
  59. package/ui/Model/AdminPanel.tsx +29 -11
  60. package/ui/Signal/Doc.tsx +3 -3
  61. package/ui/Signal/RestApi.tsx +2 -2
  62. package/ui/System/CSR.tsx +1 -1
  63. package/ui/System/Client.tsx +5 -5
  64. package/ui/System/DevModeToggle.tsx +1 -1
  65. package/ui/System/Gtag.tsx +2 -2
  66. package/ui/System/Messages.tsx +2 -2
  67. package/ui/System/ThemeToggle.tsx +43 -63
  68. package/ui/Table.tsx +1 -1
  69. package/vendor/use-agentic/AgenticSurface.ts +19 -3
  70. package/vendor/use-agentic/types.ts +8 -0
  71. package/webkit/useFrameRuntime.ts +1 -1
  72. package/store/agent/AgentVisibility.ts +0 -68
  73. package/types/store/agent/AgentVisibility.d.ts +0 -21
@@ -47,6 +47,8 @@ type RuntimeFetch = typeof globalThis.fetch & {
47
47
  serializedSignal: Record<string, unknown>;
48
48
  setJwt: (jwt: string | null) => void;
49
49
  slice: Record<string, SliceMeta>;
50
+ /** Sort keys per model refName, registered from each serialized signal's filter. */
51
+ sortKeyMap?: Map<string, string[]>;
50
52
  ws: RuntimeWs;
51
53
  [key: string]: unknown;
52
54
  };
@@ -89,6 +89,11 @@ export const baseDictionary = serviceDictionary(["en", "ko"])
89
89
  removeMsg: ["Are you sure to remove?", "정말로 삭제하시겠습니까?"],
90
90
  confirmMsg: ["Are you sure to {actionType}", "진행하시겠습니까? ({actionType})"],
91
91
  perPage: ["/page", "/페이지"],
92
+ refresh: ["Refresh", "새로고침"],
93
+ cardView: ["Card view", "카드 보기"],
94
+ tableView: ["Table view", "표 보기"],
95
+ exportCsv: ["Export CSV", "CSV 내보내기"],
96
+ exportJson: ["Export JSON", "JSON 내보내기"],
92
97
  actions: ["Actions", "작업"],
93
98
  new: ["New", "신규"],
94
99
  edit: ["Edit", "수정"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "3.0.0-alpha.24",
3
+ "version": "3.0.0-alpha.26",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -1,6 +1,7 @@
1
1
  import { type Cls, ENDPOINT_META, PrimitiveRegistry, type PrimitiveScalar, SLICE_META } from "akanjs/base";
2
2
  import { Logger } from "akanjs/common";
3
3
  import { ConstantRegistry, type ConstantType } from "akanjs/constant";
4
+ import { getFilterMeta } from "akanjs/document";
4
5
  import type { LiveRegistry } from "akanjs/service";
5
6
  import type {
6
7
  ArgInfo,
@@ -90,9 +91,12 @@ export class FetchSerializer {
90
91
  for (const [key, endpointInfo] of Object.entries(endpointMeta)) {
91
92
  endpoint[key] = FetchSerializer.#serializeEndpoint(endpointInfo);
92
93
  }
94
+
95
+ const sortKeys = Object.keys(getFilterMeta(sliceCls.srv.db.filter, { allowEmpty: true })?.sort ?? {});
93
96
  return {
94
97
  ...(prefix ? { prefix } : {}),
95
98
  ...(Object.keys(slice).length ? { slice } : {}),
99
+ ...(sortKeys.length ? { filter: { filter: {}, sortKeys } } : {}),
96
100
  ...(sliceCls.getGuards.filter((g) => g.name !== "None").length
97
101
  ? { getGuards: sliceCls.getGuards.map((g) => g.name) }
98
102
  : {}),
@@ -1,147 +1,67 @@
1
1
  import { DataList } from "akanjs/base";
2
- import { Translator } from "akanjs/client";
3
- import { parseAkanI18nEnv } from "akanjs/common";
4
2
  import { ConstantRegistry, type MaskModel, mask } from "akanjs/constant";
5
- import { FetchClient } from "akanjs/fetch";
6
- import type { AgentRefusal, JsonSchema, SerializedArg, SerializedSignal } from "akanjs/signal";
7
-
8
- import { JsonSchemaBuilder } from "../../signal/schema/JsonSchemaBuilder";
3
+ import type { AgentRefusal } from "akanjs/signal";
9
4
  import type { StoreInstance } from "../storeInstance";
10
5
  import { StoreRegistry } from "../storeRegistry";
11
- import type { AgentVisibility } from "./AgentVisibility";
12
6
  import { StoreCatalogue } from "./StoreCatalogue";
13
- import type { SerializedStore, SerializedStoreAction, SerializedStoreState, StoreActionEffect } from "./types";
14
-
15
- export interface AgentTool {
16
- name: string;
17
- title?: string;
18
- description?: string;
19
- /** One flat named object, the shape MCP publishes. The bridge maps it onto the action's positional parameters. */
20
- inputSchema: JsonSchema;
21
- effect: StoreActionEffect;
22
- }
23
-
24
- /** One call the agent made, in the order it made them. */
25
- export interface AgentCall {
26
- name: string;
27
- args: Record<string, unknown>;
28
- at: Date;
29
- error?: string;
30
- }
31
-
32
- export interface AgentBridgeOptions {
33
- /** Resolves a dictionary key to its text. Defaults to the seeded `Translator` in the active locale. */
34
- resolveDescription?: (key: string) => string | undefined;
35
- }
7
+ import type { SerializedStoreState } from "./types";
36
8
 
37
9
  /**
38
- * What an in-page agent may do to the app the user is looking at.
10
+ * What an in-page agent may read out of the store the user is looking at.
39
11
  *
40
- * Every call goes through `st.do`, which is the same single dispatch point a click goes through so the agent
41
- * cannot reach past what the UI already lets this user do, the app re-renders from the write, and the user watches
42
- * the result rather than being told about it. That is why the exposure default here is the opposite of the MCP
43
- * catalogue's: an external agent's `tools/list` is an attack surface built out of names the operator never chose to
44
- * publish, while this one is the user's own session, under their own credential, with them watching.
12
+ * Reads only. Driving the app is a component's declaration to make: `st.tool` binds a name and a schema to the
13
+ * same handler the control on screen calls, so what an agent can do is what the screen offers the user and nothing
14
+ * else. A store method that no component declared is not a lever this screen has publishing one made the surface
15
+ * the bundle rather than the screen, and gave the model levers whose effect it could not see.
45
16
  *
46
- * It is deliberately not an agent. There is no model, no provider, and no key here an app wires whichever it uses
47
- * to `tools`, `call`, and `read`. The framework's half is the catalogue, the argument checking, the masking, and the
48
- * transcript; the conversation is the app's.
17
+ * A key is readable while a mounted component subscribes it through `st.use` / `st.sel` / `st.ref`, and its value
18
+ * is masked by the model that key declares. Masking is not optional even though the data mostly came from the
19
+ * server already masked: `<model>Form` holds what the *user* typed, credentials included, and an in-page agent
20
+ * ships what it reads to a remote model. The mask is by the declared model rather than by the value's class,
21
+ * because `immerify` copies a form into a plain object and the class is gone by the time anyone can ask.
49
22
  */
50
23
  export class AgentBridge {
51
24
  readonly refusals: AgentRefusal[];
52
25
 
53
26
  readonly #instance: StoreInstance;
54
- readonly #store: SerializedStore;
55
- readonly #options: AgentBridgeOptions;
56
- readonly #schema = new JsonSchemaBuilder({ refPrefix: "#/$defs/" });
57
- readonly #byName = new Map<string, SerializedStoreAction>();
58
- readonly #calls: AgentCall[] = [];
59
- readonly #visibility: AgentVisibility;
60
- readonly #allTools: AgentTool[];
61
- readonly #liveTools = new Map<string, { signature: string; tools: AgentTool[] }>();
27
+ readonly #state: { [key: string]: SerializedStoreState };
62
28
 
63
- /**
64
- * The bridge for the app running in this process: the one store every `st.do` goes through, and every signal any
65
- * client has applied. An app needs no arguments to reach its own agent surface.
66
- */
67
- static of(options: AgentBridgeOptions = {}) {
68
- return new AgentBridge(StoreRegistry.instance, FetchClient.sharedSerializedSignal, options);
29
+ /** The bridge for the app running in this process: the one store every `st.use` goes through. */
30
+ static of() {
31
+ return new AgentBridge(StoreRegistry.instance);
69
32
  }
70
33
 
71
- constructor(
72
- instance: StoreInstance,
73
- serializedSignal: Record<string, SerializedSignal>,
74
- options: AgentBridgeOptions = {},
75
- ) {
34
+ constructor(instance: StoreInstance) {
76
35
  this.#instance = instance;
77
- this.#options = options;
78
- const catalogue = new StoreCatalogue(instance, serializedSignal);
79
- this.#store = catalogue.store;
36
+ const catalogue = new StoreCatalogue(instance);
37
+ this.#state = catalogue.state;
80
38
  this.refusals = catalogue.refusals;
81
- this.#visibility = catalogue.visibility;
82
- for (const [name, action] of Object.entries(this.#store.action)) this.#byName.set(name, action);
83
- this.#allTools = Object.entries(this.#store.action).map(([name, action]) => this.#tool(name, action));
84
- }
85
-
86
- /**
87
- * The catalogued actions of the stores the rendered screen is reading right now — the live view, recomputed as
88
- * components mount and unmount. Global exposure was the first cut's behavior and it published levers the screen
89
- * does not respond to; the screen's own subscriptions are the honest boundary.
90
- */
91
- get tools(): AgentTool[] {
92
- return this.toolsFor("");
93
- }
94
-
95
- /** The live view one zone session reads: owners counted from the keys that zone's own subtree subscribes. */
96
- toolsFor(viewKey: string): AgentTool[] {
97
- const live = this.#liveOwners(viewKey);
98
- const signature = [...live].sort().join(",");
99
- const cached = this.#liveTools.get(viewKey);
100
- if (cached && cached.signature === signature) return cached.tools;
101
- const tools = this.#allTools.filter((tool) => this.#ownerLive(tool.name, live));
102
- this.#liveTools.set(viewKey, { signature, tools });
103
- return tools;
104
- }
105
-
106
- get visibility(): AgentVisibility {
107
- return this.#visibility;
108
- }
109
-
110
- #liveOwners(viewKey = "") {
111
- return this.#visibility.liveOwners(this.#instance.liveKeysIn(viewKey));
112
- }
113
-
114
- #ownerLive(name: string, live: ReadonlySet<string>) {
115
- const owner = this.#instance.actionOwners.get(name)?.refName;
116
- return !owner || live.has(owner);
117
39
  }
118
40
 
119
41
  get state(): { [key: string]: SerializedStoreState } {
120
- return this.#store.state;
121
- }
122
-
123
- get transcript(): readonly AgentCall[] {
124
- return this.#calls;
42
+ return this.#state;
125
43
  }
126
44
 
127
45
  subscribe(listener: () => void) {
128
46
  return this.#instance.subscribe(listener);
129
47
  }
130
48
 
49
+ /** The keys one view may read right now: subscribed by a mounted component and catalogued. */
50
+ readableKeys(viewKey = ""): string[] {
51
+ return [...this.#instance.liveKeysIn(viewKey).keys()].filter((key) => !!this.#state[key]).sort();
52
+ }
53
+
131
54
  /**
132
55
  * The value behind a state key, stripped of what the model marks `hidden` or `secret`.
133
56
  *
134
- * Masking is not optional here even though the data mostly came from the server already masked: `<model>Form`
135
- * holds what the *user* typed, credentials included, and an in-page agent ships what it reads to a remote model.
136
- * The mask is by the declared model rather than by the value's class, because `immerify` copies a form into a
137
- * plain object and the class is gone by the time anyone can ask.
57
+ * The key itself has to be one the screen reads, not merely one its store owns: a component subscribing
58
+ * `userList` says the screen shows a user list, and says nothing about `userForm` sitting in the same store.
138
59
  */
139
60
  read(key: string, viewKey = ""): unknown {
140
- const entry = this.#store.state[key];
61
+ const entry = this.#state[key];
141
62
  if (!entry) throw new Error(`Unknown state key: ${key}`);
142
- const owner = this.#visibility.stateOwner(key);
143
- if (owner && !this.#liveOwners(viewKey).has(owner))
144
- throw new Error(`State key "${key}" is not part of the current screen's surface.`);
63
+ if (!this.#instance.liveKeysIn(viewKey).has(key))
64
+ throw new Error(`State key "${key}" is not read by this screen, so it is not part of its surface.`);
145
65
  const value = AgentBridge.#unwrap(this.#instance.get()[key]);
146
66
  if (entry.refName && entry.modelType) {
147
67
  const model = ConstantRegistry.getModelRef(entry.refName, entry.modelType) as MaskModel;
@@ -153,162 +73,6 @@ export class AgentBridge {
153
73
  );
154
74
  }
155
75
 
156
- /**
157
- * Dispatches through `st.do`, so what happens is what happens when the user clicks.
158
- *
159
- * Arguments arrive named and are mapped onto the action's parameters in declared order. An omitted optional one
160
- * becomes `null`, which is what the slice query builders already expect; an omitted required one is refused,
161
- * because the alternative is a call that writes `undefined` into state and reports success.
162
- */
163
- async call(name: string, args: Record<string, unknown> = {}, viewKey = "") {
164
- const action = this.#byName.get(name);
165
- if (!action) throw new Error(`Unknown action: ${name}`);
166
-
167
- const record: AgentCall = { name, args, at: new Date() };
168
- this.#calls.push(record);
169
- try {
170
- if (!this.#ownerLive(name, this.#liveOwners(viewKey)))
171
- throw new Error(`Action "${name}" is not part of the current screen's surface.`);
172
- const positional = action.args.map((arg) => this.#value(name, arg, args));
173
- await this.#instance.do[name]?.(...positional);
174
- } catch (error) {
175
- record.error = error instanceof Error ? error.message : String(error);
176
- throw error;
177
- }
178
- }
179
-
180
- #value(name: string, arg: SerializedArg, args: Record<string, unknown>) {
181
- if (!(arg.name in args) || args[arg.name] === undefined) {
182
- if (arg.nullable || arg.type === "search") return null;
183
- throw new Error(`Missing argument "${arg.name}" for ${name}.`);
184
- }
185
- return AgentBridge.#checked(name, arg, args[arg.name]);
186
- }
187
-
188
- /**
189
- * Checks a value against what the argument declared, one level of array included.
190
- *
191
- * `st.do` accepts anything — it is a rest wrapper — so without this a string where an `Int` belongs is written
192
- * into state and rendered, and the agent is told the call succeeded. A model argument is checked only for being
193
- * an object: the published schema describes its fields and the endpoint validates them server-side.
194
- */
195
- static #checked(name: string, arg: SerializedArg, value: unknown): unknown {
196
- const depth = arg.arrDepth ?? 0;
197
- if (depth) {
198
- if (!Array.isArray(value)) throw new Error(`Argument "${arg.name}" of ${name} must be an array.`);
199
- return value.map((item) => AgentBridge.#checked(name, { ...arg, arrDepth: depth - 1 }, item));
200
- }
201
- if (value === null) return null;
202
- if (arg.enum) return AgentBridge.#checkedEnum(name, arg, value);
203
- switch (arg.refName) {
204
- case "String":
205
- case "ID":
206
- return AgentBridge.#assertType(name, arg, value, "string");
207
- case "Int":
208
- if (!Number.isInteger(value)) throw new Error(`Argument "${arg.name}" of ${name} must be a whole number.`);
209
- return value;
210
- case "Float":
211
- if (typeof value !== "number" || !Number.isFinite(value))
212
- throw new Error(`Argument "${arg.name}" of ${name} must be a finite number.`);
213
- return value;
214
- case "Boolean":
215
- return AgentBridge.#assertType(name, arg, value, "boolean");
216
- case "Date":
217
- return AgentBridge.#checkedDate(name, arg, value);
218
- default:
219
- if (typeof value !== "object") throw new Error(`Argument "${arg.name}" of ${name} must be an object.`);
220
- return value;
221
- }
222
- }
223
-
224
- static #assertType(name: string, arg: SerializedArg, value: unknown, type: "string" | "boolean") {
225
- if (typeof value !== type) throw new Error(`Argument "${arg.name}" of ${name} must be a ${type}.`);
226
- return value;
227
- }
228
-
229
- static #checkedEnum(name: string, arg: SerializedArg, value: unknown) {
230
- const values = arg.enum ? ConstantRegistry.enum.get(arg.enum)?.values : undefined;
231
- if (!values) return value;
232
- if (!values.includes(value as never))
233
- throw new Error(`Argument "${arg.name}" of ${name} must be one of: ${[...values].join(", ")}.`);
234
- return value;
235
- }
236
-
237
- /** An agent has no `Date`, so an ISO string is what it sends. Anything unparseable is refused rather than `Invalid Date`. */
238
- static #checkedDate(name: string, arg: SerializedArg, value: unknown) {
239
- if (value instanceof Date) return value;
240
- const parsed = typeof value === "string" ? new Date(value) : null;
241
- if (!parsed || Number.isNaN(parsed.getTime()))
242
- throw new Error(`Argument "${arg.name}" of ${name} must be an ISO 8601 date string.`);
243
- return parsed;
244
- }
245
-
246
- #tool(name: string, action: SerializedStoreAction): AgentTool {
247
- const properties = Object.fromEntries(action.args.map((arg) => [arg.name, this.#argSchema(action, arg)]));
248
- const required = action.args.filter((arg) => !arg.nullable && arg.type !== "search").map((arg) => arg.name);
249
- const defs = this.#schema.referencedSchemas(properties);
250
- return {
251
- name,
252
- ...this.#texts(action),
253
- inputSchema: {
254
- type: "object",
255
- properties,
256
- ...(required.length ? { required } : {}),
257
- additionalProperties: false,
258
- ...(Object.keys(defs).length ? { $defs: defs } : {}),
259
- },
260
- effect: action.effect,
261
- };
262
- }
263
-
264
- /**
265
- * A store action carries no argument metadata of its own, so the prose comes from wherever the argument came
266
- * from: an endpoint-named action borrows `<refName>.signal.<endpoint>.arg.<name>.desc`, and a field setter's one
267
- * argument is the field, described at `<refName>.<field>.desc`. A slice role's `page`/`limit`/`sort` are the
268
- * framework's own and have no dictionary entry to borrow.
269
- */
270
- #argSchema(action: SerializedStoreAction, arg: SerializedArg) {
271
- const description = this.#argText(action, arg);
272
- return {
273
- ...this.#schema.arg(arg),
274
- ...(description ? { description } : {}),
275
- ...(arg.example !== undefined ? { examples: [arg.example] } : {}),
276
- };
277
- }
278
-
279
- #argText({ refName, endpoint, field }: SerializedStoreAction, arg: SerializedArg) {
280
- if (!refName) return undefined;
281
- if (endpoint) return this.#text(`${refName}.signal.${endpoint}.arg.${arg.name}.desc`);
282
- return field && arg.name === field ? this.#text(`${refName}.${field}.desc`) : undefined;
283
- }
284
-
285
- /**
286
- * The words an agent picks the action by, borrowed from what the action is named after.
287
- *
288
- * The endpoint comes first, and it is right rather than merely adequate: the house rule makes `st.do.X` and
289
- * `fetch.X` the same verb, so an action named after an endpoint reads as that endpoint's `.desc()`. A field
290
- * setter falls back to the field's own label.
291
- */
292
- #texts({ refName, endpoint, field }: SerializedStoreAction) {
293
- const keys = refName
294
- ? [...(endpoint ? [`${refName}.signal.${endpoint}`] : []), ...(field ? [`${refName}.${field}`] : [])]
295
- : [];
296
- for (const key of keys) {
297
- const title = this.#text(key);
298
- const description = this.#text(`${key}.desc`);
299
- if (title || description) return { ...(title ? { title } : {}), ...(description ? { description } : {}) };
300
- }
301
- return {};
302
- }
303
-
304
- #text(key: string) {
305
- if (this.#options.resolveDescription) return this.#options.resolveDescription(key);
306
- const locale = Translator.getActiveLocale() ?? parseAkanI18nEnv().defaultLocale;
307
- const text = Translator.translateByLocale(locale, key);
308
-
309
- return text === key ? undefined : text;
310
- }
311
-
312
76
  static #unwrap(value: unknown) {
313
77
  return value instanceof DataList ? value.values : value;
314
78
  }
@@ -8,8 +8,8 @@ import { ensureStoreSurface } from "./storeSurface";
8
8
  * The akan default context for a turn: where the user is (route), what is on screen (scopes and their curated
9
9
  * resources), and which store keys the mounted components are reading (live keys). Live entries carry names and
10
10
  * small primitives only — anything bigger is one `readState` call away, masked — so the block cannot bloat with
11
- * what happens to be in the store. Base-store keys are excluded by the base store's own `agent: false`
12
- * declaration, and the route block carries the three of them that matter.
11
+ * what happens to be in the store. Base-store plumbing is kept off the surface by `st.use.x({ agent: false })`
12
+ * at each call site, and the route block carries the three of them that matter.
13
13
  */
14
14
  export class AgentContext {
15
15
  static of(): AgentContext {
@@ -40,10 +40,7 @@ export class AgentContext {
40
40
  }
41
41
 
42
42
  #liveBlock(viewKey: string): ContextBlock[] {
43
- const live = [...this.#instance.liveKeysIn(viewKey).keys()]
44
- .filter((key) => this.#bridge.visibility.visibleKey(key))
45
- .sort()
46
- .map((key) => this.#liveEntry(key));
43
+ const live = this.#bridge.readableKeys(viewKey).map((key) => this.#liveEntry(key));
47
44
  return live.length ? [{ kind: "state", live, note: "Call readState(key) to read a value." }] : [];
48
45
  }
49
46