akanjs 3.0.0-beta.7 → 3.0.0-beta.9

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 (60) hide show
  1. package/common/index.ts +1 -0
  2. package/common/pathGet.ts +12 -2
  3. package/common/pathSet.ts +2 -3
  4. package/common/toPathSegments.ts +10 -0
  5. package/dictionary/base.dictionary.ts +5 -0
  6. package/index.ts +5 -0
  7. package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
  8. package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
  9. package/package.json +1 -1
  10. package/service/agent.service.ts +92 -1
  11. package/service/predefinedAdaptor/llm.adaptor.ts +17 -0
  12. package/store/agentic/index.ts +1 -0
  13. package/store/agentic/useAgentReference.ts +49 -0
  14. package/store/hooks.ts +1 -1
  15. package/types/common/index.d.ts +1 -0
  16. package/types/common/toPathSegments.d.ts +9 -0
  17. package/types/dictionary/base.dictionary.d.ts +1 -1
  18. package/types/dictionary/dictionary.d.ts +8 -8
  19. package/types/index.d.ts +5 -0
  20. package/types/service/agent.service.d.ts +54 -0
  21. package/types/service/predefinedAdaptor/llm.adaptor.d.ts +16 -0
  22. package/types/store/agentic/index.d.ts +1 -0
  23. package/types/store/agentic/useAgentReference.d.ts +32 -0
  24. package/types/store/hooks.d.ts +1 -1
  25. package/types/ui/Agent/Chat.d.ts +11 -1
  26. package/types/ui/Agent/Composer.d.ts +9 -2
  27. package/types/ui/Agent/Menu.d.ts +4 -2
  28. package/types/ui/Agent/Refer.d.ts +13 -0
  29. package/types/ui/Agent/useChatQueue.d.ts +3 -1
  30. package/types/ui/Agent/useChatReferences.d.ts +26 -0
  31. package/types/ui/Agent/useReferenceMenu.d.ts +43 -0
  32. package/types/ui/Field/lightRefCache.d.ts +19 -0
  33. package/types/ui/Field/useRelationOptions.d.ts +39 -0
  34. package/types/ui/Select.d.ts +2 -0
  35. package/types/ui/index.d.ts +4 -1
  36. package/types/vendor/use-agentic/AgentSession.d.ts +54 -1
  37. package/types/vendor/use-agentic/Reference.d.ts +56 -0
  38. package/types/vendor/use-agentic/index.d.ts +1 -0
  39. package/types/vendor/use-agentic/types.d.ts +36 -6
  40. package/ui/Agent/Bubble.tsx +2 -0
  41. package/ui/Agent/Chat.tsx +38 -8
  42. package/ui/Agent/Composer.tsx +18 -1
  43. package/ui/Agent/Menu.tsx +8 -3
  44. package/ui/Agent/Queued.tsx +2 -0
  45. package/ui/Agent/Refer.tsx +44 -0
  46. package/ui/Agent/sessionHistory.ts +35 -12
  47. package/ui/Agent/useChatQueue.ts +11 -1
  48. package/ui/Agent/useChatReferences.ts +67 -0
  49. package/ui/Agent/useReferenceMenu.ts +108 -0
  50. package/ui/Field/Relation.tsx +70 -150
  51. package/ui/Field/lightRefCache.ts +73 -0
  52. package/ui/Field/useRelationOptions.ts +106 -0
  53. package/ui/Select.tsx +24 -14
  54. package/ui/index.ts +8 -0
  55. package/vendor/use-agentic/AgentSession.ts +127 -1
  56. package/vendor/use-agentic/Compaction.ts +6 -0
  57. package/vendor/use-agentic/Reference.ts +99 -0
  58. package/vendor/use-agentic/Transcript.ts +7 -1
  59. package/vendor/use-agentic/index.ts +1 -0
  60. package/vendor/use-agentic/types.ts +38 -7
@@ -0,0 +1,32 @@
1
+ import { type AgentFieldType, type AgentValueOf } from "./AgentValue.d.ts";
2
+ /**
3
+ * What a component hands over when the user points at data it is already drawing.
4
+ *
5
+ * The pointer and the value are declared apart on purpose. `refName`/`refId`/`path` say *what was pointed at*, and
6
+ * travel so the agent can read it again later; `type` and `value` say *what is being shown to the model now*, and
7
+ * the component supplies the value it already holds, so there is no round trip. They come apart because the two
8
+ * are genuinely different in the case this exists for: a rich-text field stored as `field(Any)` is an editor
9
+ * document at its path and a paragraph of prose to a reader, and the model wants the paragraph.
10
+ *
11
+ * `type` is `st.expose`'s vocabulary, not a second one. It decides what leaves the browser — a model class masks
12
+ * by that model, a scalar passes, `Any` passes untouched — so naming a `Light` class that does not carry the field
13
+ * is how a reference arrives empty.
14
+ */
15
+ export interface AgentReferenceInput<T extends AgentFieldType> {
16
+ refName: string;
17
+ refId: string;
18
+ /** What the chip draws and what the token in the draft spells. */
19
+ label: string;
20
+ /** A dotted path into the document, in `pathSet`'s vocabulary. Absent points at the whole of it. */
21
+ path?: string;
22
+ type: T;
23
+ value: AgentValueOf<T>;
24
+ }
25
+ /**
26
+ * Points the enclosing agent session at data, from anywhere that draws it.
27
+ *
28
+ * No-ops outside a session rather than throwing, the same call `AgentValue.publishable` makes: a card carrying a
29
+ * reference button is mounted on whatever routes render it, and a route that happens to host no agent must not
30
+ * lose its render over it.
31
+ */
32
+ export declare const useAgentReference: () => <T extends AgentFieldType>({ type, value, ...pointer }: AgentReferenceInput<T>) => void;
@@ -1 +1 @@
1
- export { useEffect, useRef, useSyncExternalStore } from "react";
1
+ export { useContext, useEffect, useRef, useSyncExternalStore } from "react";
@@ -3,6 +3,7 @@ import { type AgentRunner, type AgentSessionOptions, type CompactOptions, type S
3
3
  import type { AttachLimits, AttachReader } from "./attachment.d.ts";
4
4
  import type { PersistOption } from "./sessionHistory.d.ts";
5
5
  import type { BuiltinOption } from "./sessionView.d.ts";
6
+ import { type ReferenceSource } from "./useReferenceMenu.d.ts";
6
7
  import type { VoiceEngine } from "./voice.d.ts";
7
8
  export interface ChatProps {
8
9
  /** Reaches whichever surface is showing — the launcher while closed, the panel while open. */
@@ -78,6 +79,15 @@ export interface ChatProps {
78
79
  * uploading reader gets a thumbnail without betting the answer on who can reach its storage.
79
80
  */
80
81
  attach?: AttachReader;
82
+ /**
83
+ * What the composer's `@` menu can point at — one entry per kind of document a user may name while asking.
84
+ *
85
+ * Which documents those are is the app's answer, so the source carries its own `search`; the framework carries
86
+ * the token, the masking and the snapshot. Whole documents only: a field inside one is pointed at from the
87
+ * component that draws it, with `useAgentReference`, because that component is the thing that knows a rich-text
88
+ * field reads as a paragraph rather than as the editor document it is stored as.
89
+ */
90
+ reference?: readonly ReferenceSource[];
81
91
  /**
82
92
  * Raises or lowers what the composer accepts — per file, per message, and how many. The defaults are what one
83
93
  * turn's JSON safely carries to a conservative provider; an app pointed at a larger request limit, or one whose
@@ -98,6 +108,6 @@ export interface ChatProps {
98
108
  * `persist` keeps it. An enclosing AgentProvider's session wins, which is how an app isolates a surface or swaps
99
109
  * the loop while keeping this UI.
100
110
  */
101
- export declare const DefaultChat: ({ className, title, instructions, runner, maxTurns, compact, builtins, onCompact, defaultOpen, open: openProp, onOpenChange, launcher, persist, inline, shortcut, launcherClassName, panelClassName, intro, header, chrome, defaultDraft, attach, attachLimits, voice, }: ChatProps) => ReactNode;
111
+ export declare const DefaultChat: ({ className, title, instructions, runner, maxTurns, compact, builtins, onCompact, defaultOpen, open: openProp, onOpenChange, launcher, persist, inline, shortcut, launcherClassName, panelClassName, intro, header, chrome, defaultDraft, attach, attachLimits, reference, voice, }: ChatProps) => ReactNode;
102
112
  declare const _default: import("react").ComponentType<ChatProps>;
103
113
  export default _default;
@@ -1,10 +1,15 @@
1
1
  import { type KeyboardEvent, type RefObject } from "react";
2
- import type { AgentSession, MessageAttachment } from "../../vendor/use-agentic.d.ts";
2
+ import type { AgentSession, MessageAttachment, MessageReference } from "../../vendor/use-agentic.d.ts";
3
3
  export interface ComposerProps {
4
4
  className?: string;
5
5
  session: AgentSession;
6
6
  draft: string;
7
7
  attached: readonly MessageAttachment[];
8
+ /**
9
+ * What the draft's `@[…](mention:…)` tokens point at. The text is what carries them, so a composer that draws no
10
+ * chip still sends them — the chip is how somebody sees and removes one, not how it travels.
11
+ */
12
+ references?: readonly MessageReference[];
8
13
  /** Files still being read. An `attach` that uploads takes seconds, and a panel that shows nothing looks broken. */
9
14
  pending?: number;
10
15
  /** Absent when the screen cannot listen — the same rule as publishing no tool for a control that is not drawn. */
@@ -16,10 +21,12 @@ export interface ComposerProps {
16
21
  onKeyDown: (event: KeyboardEvent<HTMLTextAreaElement>) => void;
17
22
  onFiles: (files: File[]) => void;
18
23
  onRemoveFile: (idx: number) => void;
24
+ /** Keyed on `refName/refId#path`, not on a row index: the order is the draft's, and the draft is the truth. */
25
+ onRemoveReference?: (key: string) => void;
19
26
  onSend: () => void;
20
27
  onStop: () => void;
21
28
  inputRef?: RefObject<HTMLTextAreaElement | null>;
22
29
  }
23
30
  /** What the user writes with: the staged files above, and the controls that send or stop below them. */
24
- export declare const DefaultComposer: ({ className, session, draft, attached, pending, mic, onDraft, onKeyDown, onFiles, onRemoveFile, onSend, onStop, inputRef, }: ComposerProps) => import("react/jsx-runtime").JSX.Element;
31
+ export declare const DefaultComposer: ({ className, session, draft, attached, references, pending, mic, onDraft, onKeyDown, onFiles, onRemoveFile, onRemoveReference, onSend, onStop, inputRef, }: ComposerProps) => import("react/jsx-runtime").JSX.Element;
25
32
  export declare const Composer: import("react").ComponentType<ComposerProps>;
@@ -10,9 +10,11 @@ export interface MenuProps {
10
10
  rows: MenuRow[];
11
11
  /** Index the arrows are on. Enter picks it and Tab completes its name, so it has to be visible. */
12
12
  selected: number;
13
+ /** What the rows are being typed after, so a name reads as the thing the user is completing. */
14
+ prefix?: string;
13
15
  onPick: (row: MenuRow) => void;
14
16
  }
15
- /** The `/` menu: this chat's own commands first, then the app's `prompt()` endpoints. */
16
- export declare const DefaultAgentMenu: ({ className, rows, selected, onPick }: MenuProps) => import("react/jsx-runtime").JSX.Element | null;
17
+ /** The composer's completion list: `/` commands and `prompt()` endpoints, or the `@` menu's reference rows. */
18
+ export declare const DefaultAgentMenu: ({ className, rows, selected, prefix, onPick }: MenuProps) => import("react/jsx-runtime").JSX.Element | null;
17
19
  declare const _default: import("react").ComponentType<MenuProps>;
18
20
  export default _default;
@@ -0,0 +1,13 @@
1
+ import { type MessageReference } from "../../vendor/use-agentic.d.ts";
2
+ export interface ReferenceChipsProps {
3
+ className?: string;
4
+ references: readonly MessageReference[];
5
+ /** Omitted for a sent message: what is already on the wire cannot be taken back. */
6
+ onRemove?: (key: string) => void;
7
+ removeLabel?: string;
8
+ }
9
+ /**
10
+ * What the user pointed at, beside what they are typing. Keyed on the pointer rather than on the row's position:
11
+ * the order a message's references appear in is its text's, and removing one is removing its token.
12
+ */
13
+ export declare const ReferenceChips: ({ className, references, onRemove, removeLabel }: ReferenceChipsProps) => import("react/jsx-runtime").JSX.Element;
@@ -1,9 +1,11 @@
1
- import type { AgentSession, MessageAttachment } from "../../vendor/use-agentic.d.ts";
1
+ import type { AgentSession, MessageAttachment, MessageReference } from "../../vendor/use-agentic.d.ts";
2
2
  import { type AttachLimits } from "./attachment.d.ts";
3
3
  /** What the composer held when it was sent: the shape a turn opens with, whether now or after the running one. */
4
4
  export interface QueuedMessage {
5
5
  text: string;
6
6
  attachments: MessageAttachment[];
7
+ /** What the parked text's tokens pointed at. Carried so a turn that opens later still knows what they held. */
8
+ references: MessageReference[];
7
9
  /** The ask came from the microphone, so the reply it gets is read aloud — carried until the send that opens it. */
8
10
  byVoice: boolean;
9
11
  }
@@ -0,0 +1,26 @@
1
+ import { type RefObject } from "react";
2
+ import { type AgentSession, type MessageReference } from "../../vendor/use-agentic.d.ts";
3
+ interface ChatReferencesSetup {
4
+ session: AgentSession;
5
+ draft: string;
6
+ /** The chat's own version snapshot — a `refer` from anywhere on the page is one of the changes it counts. */
7
+ version: number;
8
+ inputRef: RefObject<HTMLTextAreaElement | null>;
9
+ onDraft: (text: string) => void;
10
+ }
11
+ /**
12
+ * The composer's half of a reference: writing the token for one somebody pointed at elsewhere, and reading back
13
+ * which references the draft now carries.
14
+ *
15
+ * **The text is the source of truth, and the staged values are looked up beside it.** A reference is on the
16
+ * message because its token is in the message — so deleting the token by hand drops it, exactly as deleting the
17
+ * chip does, and neither can leave the other behind. The draft is never scanned to re-derive a value; a token
18
+ * whose value was never staged (pasted out of an earlier message) travels as a pointer with a note saying to read
19
+ * it again, which is the same shape a restored conversation produces.
20
+ */
21
+ export declare const useChatReferences: ({ session, draft, version, inputRef, onDraft }: ChatReferencesSetup) => {
22
+ references: MessageReference[];
23
+ /** Removing the chip removes the token, because the token is what puts the reference on the message. */
24
+ remove: (key: string) => void;
25
+ };
26
+ export {};
@@ -0,0 +1,43 @@
1
+ import { type AgentFieldType } from "akanjs/store";
2
+ import { type AgentSession } from "../../vendor/use-agentic.d.ts";
3
+ import type { MenuRow } from "./Menu.d.ts";
4
+ /** One row the `@` menu offers: a document of its source's model, named the way the screen names it. */
5
+ export interface ReferenceCandidate {
6
+ refId: string;
7
+ label: string;
8
+ description?: string;
9
+ }
10
+ /**
11
+ * A kind of document the `@` menu can point at.
12
+ *
13
+ * `type` is the whole decision about what leaves the browser, in `st.expose`'s vocabulary — a model class masks by
14
+ * that model. **Name the class that actually carries the fields somebody is pointing at**: a `Light<Model>` is
15
+ * usually not it, and a reference masked by one arrives without the field that was the reason for pointing.
16
+ *
17
+ * `search` is the app's own query, because which documents a person may point at is the app's answer and not the
18
+ * framework's; the signal is aborted when the query moves on. `resolve` is called once, when a row is picked.
19
+ */
20
+ export interface ReferenceSource<T extends AgentFieldType = AgentFieldType> {
21
+ refName: string;
22
+ /** What this group of rows is called in the menu. */
23
+ label: string;
24
+ type: T;
25
+ search: (query: string, signal: AbortSignal) => Promise<ReferenceCandidate[]>;
26
+ resolve: (refId: string) => Promise<unknown>;
27
+ }
28
+ interface ReferenceMenuSetup {
29
+ draft: string;
30
+ sources: readonly ReferenceSource[];
31
+ session: AgentSession;
32
+ l: (key: string, param?: Record<string, string | number>) => string;
33
+ onWrite: (text: string) => void;
34
+ }
35
+ export declare const useReferenceMenu: ({ draft, sources, session, l, onWrite }: ReferenceMenuSetup) => {
36
+ rows: MenuRow[];
37
+ selected: number;
38
+ reopen: () => void;
39
+ hide: () => void;
40
+ move: (delta: number) => void;
41
+ at: () => MenuRow | undefined;
42
+ };
43
+ export {};
@@ -0,0 +1,19 @@
1
+ interface LightRow {
2
+ id: string;
3
+ }
4
+ /**
5
+ * Light rows read one id at a time, for a control holding an id its option list does not carry — a form opened
6
+ * through `edit<Model>` before the dropdown was ever opened, or a row outside the one `limit`-sized window a
7
+ * slice list holds.
8
+ *
9
+ * Deliberately not the model's store: that store is a singleton, so reading into it would overwrite whatever
10
+ * listing of the same model is already on the screen — the same reason `Data/RefPicker` keeps its rows local.
11
+ */
12
+ export declare class LightRefCache {
13
+ #private;
14
+ static get<Light extends LightRow>(refName: string, id: string): Light | null;
15
+ static subscribe(listener: (refName: string, id: string) => void): () => void;
16
+ static load(refName: string, ids: string[]): void;
17
+ static reset(): void;
18
+ }
19
+ export {};
@@ -0,0 +1,39 @@
1
+ import { DataList } from "akanjs/base";
2
+ import type { SliceMeta } from "akanjs/fetch";
3
+ import { type ReactNode } from "react";
4
+ export interface RelationOptionsSource<Light extends {
5
+ id: string;
6
+ }> {
7
+ slice: SliceMeta;
8
+ /** Ids the control holds right now, whether or not the option list carries them. */
9
+ ids: string[];
10
+ /** Rows the control was handed outright, which a `Light`-valued field's own value already is. */
11
+ pinned?: (Light | null)[];
12
+ initArgs?: any[];
13
+ sortOption?: (a: Light, b: Light) => number;
14
+ renderOption?: (model: Light) => ReactNode;
15
+ }
16
+ /**
17
+ * The options a relation control offers, which are never only the slice list: that list arrives when the dropdown
18
+ * opens and holds one page when it does, so a control rendering a value the list does not carry would render an
19
+ * empty field over a form that holds the value. What is missing from it is filled from the value the control was
20
+ * handed, and — for an id-valued control, which holds no row at all — read by id through {@link LightRefCache}.
21
+ */
22
+ export declare const useRelationOptions: <Light extends {
23
+ id: string;
24
+ }>({ slice, ids, pinned, initArgs, sortOption, renderOption, }: RelationOptionsSource<Light>) => {
25
+ models: DataList<Light>;
26
+ options: {
27
+ label: string;
28
+ value: string;
29
+ }[];
30
+ optionLabel: (model: Light) => string;
31
+ listLoading: boolean;
32
+ /**
33
+ * `invalidate: false` so an open reuses a list the page already loaded rather than replacing it: the slice
34
+ * list is one store key, so refetching it here overwrites whatever listing of the same model is on screen.
35
+ */
36
+ load: () => Promise<void>;
37
+ /** Live rather than the rendered list, because the agent's list tool loads and reads within one call. */
38
+ read: () => DataList<Light>;
39
+ };
@@ -28,6 +28,8 @@ export interface SelectProps<T extends string | number | boolean | null | undefi
28
28
  nullable?: boolean;
29
29
  /** Disable open and selection behavior. */
30
30
  disabled?: boolean;
31
+ /** The options are still being loaded: shows a spinner in place of the empty placeholder. */
32
+ loading?: boolean;
31
33
  /** Called when the dropdown opens. */
32
34
  onOpen?: () => void;
33
35
  /** Controlled change callback. Receives next and previous value. */
@@ -1,4 +1,5 @@
1
- export { AgentProvider, type AgentProviderProps, type AgentRunner, AgentSession, type AgentSessionOptions, type ChatMessage, type CompactOptions, type ContextBlock, httpRunner, type MessageAttachment, type PublishedTool, type RunnerEvent, type RunnerRequest, SessionContext, type SessionHistory, type SurfaceView, useAgent, } from "../vendor/use-agentic.d.ts";
1
+ export { type AgentReferenceInput, useAgentReference } from "akanjs/store";
2
+ export { AgentProvider, type AgentProviderProps, type AgentRunner, AgentSession, type AgentSessionOptions, type ChatMessage, type CompactOptions, type ContextBlock, httpRunner, type MessageAttachment, type MessageReference, type PublishedTool, Reference, type RunnerEvent, type RunnerRequest, SessionContext, type SessionHistory, type SurfaceView, useAgent, } from "../vendor/use-agentic.d.ts";
2
3
  export { Agent } from "./Agent.d.ts";
3
4
  export { type ApprovalProps, DefaultApproval } from "./Agent/Approval.d.ts";
4
5
  export { Chips as AgentAttachments, type ChipsProps as AgentAttachmentsProps } from "./Agent/Attach.d.ts";
@@ -15,10 +16,12 @@ export { type CodeProps, DefaultCode, DefaultMarkdown, type MarkdownProps } from
15
16
  export { DefaultAgentMenu, type MenuProps as AgentMenuProps, type MenuRow } from "./Agent/Menu.d.ts";
16
17
  export { DefaultQuestion, type QuestionProps } from "./Agent/Question.d.ts";
17
18
  export { DefaultQueued, type QueuedProps } from "./Agent/Queued.d.ts";
19
+ export { ReferenceChips as AgentReferences, type ReferenceChipsProps as AgentReferencesProps, } from "./Agent/Refer.d.ts";
18
20
  export type { PersistOption } from "./Agent/sessionHistory.d.ts";
19
21
  export type { AgentBuiltin, BuiltinOption } from "./Agent/sessionView.d.ts";
20
22
  export { tokenCount } from "./Agent/tokenCount.d.ts";
21
23
  export type { QueuedMessage } from "./Agent/useChatQueue.d.ts";
24
+ export type { ReferenceCandidate, ReferenceSource } from "./Agent/useReferenceMenu.d.ts";
22
25
  export type { VoiceEngine, VoiceHandlers, VoiceListener, VoiceSpeech } from "./Agent/voice.d.ts";
23
26
  export { agentAttrs } from "./agentAttrs.d.ts";
24
27
  export { animated } from "./animated.d.ts";
@@ -1,6 +1,6 @@
1
1
  import type { AgentProgressReport } from "./AgentProgress.d.ts";
2
2
  import { type CompactOptions } from "./Compaction.d.ts";
3
- import type { AgentRunner, ChatMessage, ContextBlock, PublishedTool, SurfaceView } from "./types.d.ts";
3
+ import type { AgentRunner, ChatMessage, ContextBlock, MessageReference, PublishedTool, SurfaceView } from "./types.d.ts";
4
4
  export interface PendingApproval {
5
5
  callId: string;
6
6
  name: string;
@@ -111,6 +111,59 @@ export declare class AgentSession {
111
111
  get isCompacting(): boolean;
112
112
  get pendingApproval(): PendingApproval | null;
113
113
  get pendingQuestion(): PendingQuestion | null;
114
+ /**
115
+ * What the user has pointed at and not yet sent.
116
+ *
117
+ * Held by the session rather than by the composer, which is where staged *files* live — and the difference is
118
+ * not an inconsistency. A file only ever arrives from the composer's own picker or drop zone, so composer-local
119
+ * state can reach every producer of one. A reference arrives from whichever component drew the data: a card
120
+ * partway down the page, reaching the session it is already inside. Composer state is unreachable from there,
121
+ * and a chat that replaces its composer has no state to reach anyway.
122
+ *
123
+ * A staging slot, not a tray. `send` empties it, so what somebody pointed at belongs to the message they were
124
+ * writing and never leaks into the next one.
125
+ */
126
+ get staged(): readonly MessageReference[];
127
+ /**
128
+ * Pointing at the same field twice replaces it: the newer value is the one they meant, and one chip is honest.
129
+ *
130
+ * Stages the value only. The caller is the composer's own `@` menu, which is already writing the token as the
131
+ * user picks — `refer` is the entry point for everything that is not the composer.
132
+ */
133
+ stage: (reference: MessageReference) => void;
134
+ /**
135
+ * Points at something from a component that is not the composer — a card the user clicked beside the data.
136
+ *
137
+ * Two halves, because the composer owns one of them: the value is staged here, and the token the message needs
138
+ * is left for whichever chat is rendering this session's draft to write. That is why nothing is queued when no
139
+ * chat is: the value would sit staged with no token anywhere naming it, and the message text is what decides
140
+ * which references a turn carries — so the user would press a button and watch nothing happen, which is the
141
+ * failure this warns about instead.
142
+ */
143
+ refer: (reference: MessageReference) => void;
144
+ /**
145
+ * Tokens a chat has not written into its draft yet. State rather than a queue somebody drains, for the reason
146
+ * `pendingApproval` is state: two chats may be mounted on one session — a responsive app renders a desktop and
147
+ * a mobile composer and hides one in CSS — and each holds its own draft, so each has to write the token. A
148
+ * destructive read would hand it to whichever rendered first and leave the other silently without it.
149
+ */
150
+ get pendingInserts(): readonly MessageReference[];
151
+ /** Idempotent by key: the second chat to apply the same insert is acknowledging one that is already gone. */
152
+ insertApplied: (key: string) => void;
153
+ /**
154
+ * Stages references back from a message that was parked behind a running turn. What was pointed at since wins:
155
+ * the parked value is the older read of the same field, and letting it land would undo an edit made in between.
156
+ * No token is written — the parked text carries them already, and the composer is putting that text back.
157
+ */
158
+ restoreStaged: (references: readonly MessageReference[]) => void;
159
+ /**
160
+ * Registered by a chat for as long as it is rendering this session's draft, so `refer` can tell the difference
161
+ * between a token nobody has written yet and one nobody ever will.
162
+ */
163
+ attachChat: () => () => void;
164
+ /** By key, never by index: what orders the references of a message is its text, and that is not this list. */
165
+ unstage: (key: string) => void;
166
+ clearStaged: () => void;
114
167
  /** What the tool running now last said about its own progress, for the row that is still spinning. */
115
168
  get progress(): (AgentProgressReport & {
116
169
  callId: string;
@@ -0,0 +1,56 @@
1
+ import type { MessageReference } from "./types.d.ts";
2
+ /**
3
+ * The ceiling on what one reference may add to the transcript, and the identity two of them are the same by.
4
+ *
5
+ * A reference is bulkier than it looks and lasts longer than a tool result. The host hands over whatever the
6
+ * screen was already holding — a document with an array of rows in it is tens of kilobytes — and unlike a tool
7
+ * result, which answers one turn's question, a reference is part of a *user* message: it rides the wire on this
8
+ * turn and on every turn after it, and it is the last thing compaction folds, because folding the thing the user
9
+ * pointed at is folding the question.
10
+ *
11
+ * So the value is bounded where it is staged, before it ever enters a message. Clipped rather than dropped, and
12
+ * the note says which: a model shown a value it cannot see the end of asks a narrower question, where one shown
13
+ * nothing answers from the field names.
14
+ */
15
+ export declare class Reference {
16
+ #private;
17
+ /**
18
+ * Characters. The same number as `ToolOutput.limit` and for the same reason — far below any provider's window,
19
+ * comfortably above what one record needs — but its own constant, because the two bound different things and a
20
+ * host that finds one too generous has no reason to have found the other so.
21
+ */
22
+ static readonly limit = 20000;
23
+ /**
24
+ * What the token in the message text spells, so the text is the index into the values and neither can drift
25
+ * from the other. A path is part of the identity: two fields of one document are two references.
26
+ */
27
+ static keyOf({ refName, refId, path }: MessageReference): string;
28
+ static same(one: MessageReference, other: MessageReference): boolean;
29
+ /**
30
+ * The token a reference reads as in the message the user is writing: `@[label](mention:refName/id#path)`.
31
+ *
32
+ * The text is the source of truth for which references a message carries — deleting the token is how somebody
33
+ * takes one back — so the token has to say everything the pointer does. The value is looked up beside it by key.
34
+ */
35
+ static token(reference: MessageReference): string;
36
+ /** Greedy on the label alone: a label may hold anything but `]`, and every other part is a name or an id. */
37
+ static readonly pattern: RegExp;
38
+ /**
39
+ * The pointers a draft names, in the order it names them. Pointers only — the value lives in `staged` and is
40
+ * joined on by key, so reading the text can never resurrect a value somebody deleted the token for.
41
+ */
42
+ static parse(text: string): MessageReference[];
43
+ /**
44
+ * A value past the ceiling becomes the JSON text up to it. That leaves `value` a string holding a fragment of a
45
+ * structure, which is exactly what it is — and a reader renders a string value as itself, so the model sees the
46
+ * fragment rather than an escaped quotation of one.
47
+ */
48
+ static clipped(reference: MessageReference): MessageReference;
49
+ /**
50
+ * The text with every token naming `key` taken out, and the gap it leaves closed up. Deleting the chip and
51
+ * deleting the token have to be the same act, because the text is what decides which references a turn carries.
52
+ */
53
+ static without(text: string, key: string): string;
54
+ /** What a pasted token resolves to: the pointer is in the text, and no value was ever staged beside it. */
55
+ static readonly unstagedNote = "the value was not captured with this reference, so read it again with a tool before answering about it";
56
+ }
@@ -7,6 +7,7 @@ export * from "./AgentScope.d.ts";
7
7
  export * from "./AgentSession.d.ts";
8
8
  export * from "./Compaction.d.ts";
9
9
  export * from "./httpRunner.d.ts";
10
+ export * from "./Reference.d.ts";
10
11
  export * from "./sharedContext.d.ts";
11
12
  export * from "./surfaceContext.d.ts";
12
13
  export * from "./ToolOutput.d.ts";
@@ -101,6 +101,12 @@ export interface ToolCallResult {
101
101
  changes?: ResourceDiff[];
102
102
  error?: string;
103
103
  }
104
+ /**
105
+ * Why an assistant turn ended. `length` is the provider's own ceiling rather than the model's choice, so the turn
106
+ * is incomplete — a truncated answer and a turn cut off before its tool call finished both arrive this way, and
107
+ * neither is distinguishable from `end` without it.
108
+ */
109
+ export type TurnStop = "end" | "toolUse" | "length";
104
110
  /**
105
111
  * A file the user handed the conversation rather than the screen — which is why it rides a message instead of a
106
112
  * tool, the same reason `askUser` belongs to the session and not to the surface.
@@ -114,12 +120,6 @@ export interface ToolCallResult {
114
120
  * model cannot read and says so in the transcript, because a silently dropped file is one the model then
115
121
  * hallucinates about.
116
122
  */
117
- /**
118
- * Why an assistant turn ended. `length` is the provider's own ceiling rather than the model's choice, so the turn
119
- * is incomplete — a truncated answer and a turn cut off before its tool call finished both arrive this way, and
120
- * neither is distinguishable from `end` without it.
121
- */
122
- export type TurnStop = "end" | "toolUse" | "length";
123
123
  export interface MessageAttachment {
124
124
  name: string;
125
125
  mimeType: string;
@@ -135,11 +135,41 @@ export interface MessageAttachment {
135
135
  */
136
136
  ref?: string;
137
137
  }
138
+ /**
139
+ * Data the user pointed at while they were talking, rather than a file they handed over — a record, or one field
140
+ * of one, named in the message the way they named it. It rides a message for the same reason an attachment does:
141
+ * what somebody referred to while asking is part of the asking, and the turn context is rebuilt from the screen
142
+ * every turn, so a screen-shaped carrier forgets what was pointed at three turns ago.
143
+ *
144
+ * **`value` is a snapshot, deliberately.** It is what the data was when the user sent the message, and it is never
145
+ * re-read on a later turn. Re-reading would be wrong twice over: it rewrites what the person was looking at when
146
+ * they spoke, and the common case is an agent that then *edits* the very field it was pointed at, which would
147
+ * leave the reference showing the result and no record of what was being changed from. `refName`, `refId` and
148
+ * `path` are the way back to the current value — a tool re-reads it when the answer needs it.
149
+ *
150
+ * **`value` arrives masked, and nothing downstream can mask it again.** Masking needs the model class
151
+ * (`mask(model, value)`), which no wire carries, so whichever model the host names when it stages the reference is
152
+ * the whole of the decision about what leaves the browser.
153
+ */
154
+ export interface MessageReference {
155
+ /** The host's own `refName`, unchanged — the vocabulary its published tools already speak. */
156
+ refName: string;
157
+ refId: string;
158
+ /** What the chip draws and what the token in the text spells, so the two can never disagree. */
159
+ label: string;
160
+ /** A dotted path into the document, in `pathSet`'s vocabulary. Absent means the whole of it. */
161
+ path?: string;
162
+ value?: unknown;
163
+ /** Read by the model in place of a value there is none of — clipped, unreadable, or gone from a restored chat. */
164
+ note?: string;
165
+ }
138
166
  export interface ChatMessage {
139
167
  role: ChatRole;
140
168
  text?: string;
141
169
  /** Files the message carries. Content, not instructions — a backend frames them the way it frames context. */
142
170
  attachments?: MessageAttachment[];
171
+ /** Data the message points at. Content, framed like attachments and for the same reason. */
172
+ references?: MessageReference[];
143
173
  toolCalls?: ToolCallRequest[];
144
174
  toolResults?: ToolCallResult[];
145
175
  /** A failed or capped turn, recorded in the transcript rather than thrown past it. */
@@ -5,6 +5,7 @@ import { type AgentProgressReport, AgentSession, type ChatMessage, type ToolCall
5
5
  import { createOverridable } from "../UiOverride";
6
6
  import { Chips } from "./Attach";
7
7
  import Markdown from "./Markdown";
8
+ import { ReferenceChips } from "./Refer";
8
9
  import { tokenCount } from "./tokenCount";
9
10
 
10
11
  export interface BubbleProps {
@@ -150,6 +151,7 @@ const Content = ({ className, message, progress, results }: BubbleProps) => {
150
151
  return (
151
152
  <div className={cn("flex max-w-[85%] flex-col items-end gap-1 self-end", className)}>
152
153
  {message.attachments?.length ? <Chips attachments={message.attachments} className="justify-end" /> : null}
154
+ {message.references?.length ? <ReferenceChips className="justify-end" references={message.references} /> : null}
153
155
  {message.text ? (
154
156
  <p className="whitespace-pre-wrap rounded-box bg-primary/10 px-3 py-2 text-sm">{message.text}</p>
155
157
  ) : null}