@farmslot/protocol 0.74.4 → 0.74.5
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/CHANGELOG.md +12 -0
- package/dist/client-cache/index.d.ts +1 -0
- package/dist/client-cache/index.d.ts.map +1 -1
- package/dist/client-cache/index.js +1 -0
- package/dist/client-cache/index.js.map +1 -1
- package/dist/client-cache/native-history.d.ts +94 -4
- package/dist/client-cache/native-history.d.ts.map +1 -1
- package/dist/client-cache/native-history.js +332 -13
- package/dist/client-cache/native-history.js.map +1 -1
- package/dist/client-cache/terminal-text.d.ts +6 -0
- package/dist/client-cache/terminal-text.d.ts.map +1 -0
- package/dist/client-cache/terminal-text.js +27 -0
- package/dist/client-cache/terminal-text.js.map +1 -0
- package/dist/contracts/family.d.ts +2 -1
- package/dist/contracts/family.d.ts.map +1 -1
- package/dist/contracts/family.js.map +1 -1
- package/dist/contracts/runs.d.ts +6 -0
- package/dist/contracts/runs.d.ts.map +1 -1
- package/dist/contracts/runs.js.map +1 -1
- package/dist/rpc/native-session.d.ts +20 -6
- package/dist/rpc/native-session.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,18 @@ All notable changes to `@farmslot/protocol` are tracked here.
|
|
|
6
6
|
|
|
7
7
|
- Active-development baseline; add user-facing changes here before release or package publication.
|
|
8
8
|
|
|
9
|
+
## 0.74.5 - 2026-10-31
|
|
10
|
+
|
|
11
|
+
- `nativeSendLabel` gives a native composer's send action for a session state: "Send" when idle, "Queue message" for the next turn while a turn runs or waits on a request, none when the session takes no input. `nativeHistoryCanSend` is the held-data rule for sending a message: current, complete history of the conversation on screen (same generation, worker lease and task scope, not released), without `nativeHistoryCanAct`'s requirement that it be the exact snapshot on screen, so a newer read landing while a turn streams does not refuse a queued message.
|
|
12
|
+
- `RunEngineState.taskInit` records whether a run's task still waits for node initialization after prepare (`pending`) or is `initialized`.
|
|
13
|
+
- `NativeSessionEvent.tool` is `NativeSessionTool`, with optional `kind` (`NativeToolKind`: `command`, `file-edit`, `file-read`, `search`, `web`, `other`), `command` and `paths` set by the runner adapter. `nativeToolLabel` reads a tool call as "Run npm test", "Change a.ts, b.ts", "Read a.ts" or "Search src", or the runner's name; native timeline entries and `nativeWorkLabel` use it. `nativeCommandText` gives a command card's text: the command's first line and its last 10 output lines, cleaned as the terminal screen cleans them. `stripTerminalControls`, `terminalLinesFromText` and `terminalTailLinesFromText` move into the client cache from Companion; `stripTerminalControls` ends an OSC sequence at its own terminator, so a terminal hyperlink (OSC 8) keeps its text.
|
|
14
|
+
- `NativeSessionEvent.response`, optional, on `approval.resolved`: the response the host delivered for the request (`decision` or `answers`); absent when the host did not deliver one. `nativeTimeline` keeps each resolved request as a `resolution` entry, "Approved: …", "Declined: …", "Answered: question → answer" or "Resolved: …". Cached native history keeps them, and a snapshot saved by an older reader is read again from its start, once.
|
|
15
|
+
- `FamilyObservabilitySnapshot.latestRunId` is optional: a reference-only eval family has no runs yet.
|
|
16
|
+
- `nativeTimelineRows` adds a summary row for each finished turn's tool calls ("Worked for 4m 12s · 6 tools", "Stopped after …", "Failed after …") just before the turn's end, and marks those tools with it; tools keep their keys, and messages, statuses and a running turn stay as they are. A turn also ends where its host stopped (`session.closed`). `nativeWatchTools` and `nativeFoldWatched` decide that a turn seen running in a live read starts open.
|
|
17
|
+
- `nativeWork` finds the turn a native session is running, when it started and its current tool; `nativeWorkLabel` gives "Working · 1m 12s · Bash". Cached native history keeps the running turn's `turn.started`, which it dropped before as not a timeline entry.
|
|
18
|
+
- `nativeMessagePreview` gives the first 8 lines (at most 600 characters) of a native conversation message over 12 lines or 900 characters, with its line count; `nativeEntryPreview` applies it to user messages only, and `nativeMessageToggleLabel` gives the "Show all · N lines" or "Show less" label.
|
|
19
|
+
- `NativeFollow`, `NATIVE_FOLLOW_LATEST`, `nativeFollowScrolled`, `nativeFollowGrew` and `nativeFollowLabel` track whether a native conversation reader follows new output and how many entries arrived while they read further up.
|
|
20
|
+
|
|
9
21
|
## 0.74.4 - 2026-10-30
|
|
10
22
|
|
|
11
23
|
- `buildGatewayPairingLink` turns a pairing QR payload into a Companion pair link (`farmslot://pair?payload=…`, or `farmslot-preview://` and `farmslot-development://` for those builds), encoded so it passes through `adb shell am start -d` unchanged.
|
|
@@ -11,6 +11,7 @@ export * from './prefetch.js';
|
|
|
11
11
|
export * from './registry.js';
|
|
12
12
|
export { RUN_LIST_COLLECTION_ID, type RunListCollectionData } from './run-list-versions.js';
|
|
13
13
|
export * from './store.js';
|
|
14
|
+
export * from './terminal-text.js';
|
|
14
15
|
export { createResourceCacheTransport, type ResourceClientConnection, type ResourceConnectionInputs, } from './transport.js';
|
|
15
16
|
export * from './types.js';
|
|
16
17
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client-cache/index.ts"],"names":[],"mappings":"AAAA,yFAAyF;AACzF,OAAO,EAAE,0BAA0B,EAAE,MAAM,YAAY,CAAC;AACxD,cAAc,qBAAqB,CAAC;AACpC,OAAO,EAAE,2BAA2B,EAAE,MAAM,kBAAkB,CAAC;AAC/D,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,WAAW,CAAC;AAC1B,cAAc,qBAAqB,CAAC;AACpC,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,sBAAsB,EAAE,KAAK,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAC5F,cAAc,YAAY,CAAC;AAC3B,OAAO,EACL,4BAA4B,EAC5B,KAAK,wBAAwB,EAC7B,KAAK,wBAAwB,GAC9B,MAAM,gBAAgB,CAAC;AACxB,cAAc,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client-cache/index.ts"],"names":[],"mappings":"AAAA,yFAAyF;AACzF,OAAO,EAAE,0BAA0B,EAAE,MAAM,YAAY,CAAC;AACxD,cAAc,qBAAqB,CAAC;AACpC,OAAO,EAAE,2BAA2B,EAAE,MAAM,kBAAkB,CAAC;AAC/D,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,WAAW,CAAC;AAC1B,cAAc,qBAAqB,CAAC;AACpC,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,sBAAsB,EAAE,KAAK,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAC5F,cAAc,YAAY,CAAC;AAC3B,cAAc,oBAAoB,CAAC;AACnC,OAAO,EACL,4BAA4B,EAC5B,KAAK,wBAAwB,EAC7B,KAAK,wBAAwB,GAC9B,MAAM,gBAAgB,CAAC;AACxB,cAAc,YAAY,CAAC"}
|
|
@@ -11,6 +11,7 @@ export * from './prefetch.js';
|
|
|
11
11
|
export * from './registry.js';
|
|
12
12
|
export { RUN_LIST_COLLECTION_ID } from './run-list-versions.js';
|
|
13
13
|
export * from './store.js';
|
|
14
|
+
export * from './terminal-text.js';
|
|
14
15
|
export { createResourceCacheTransport, } from './transport.js';
|
|
15
16
|
export * from './types.js';
|
|
16
17
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client-cache/index.ts"],"names":[],"mappings":"AAAA,yFAAyF;AACzF,OAAO,EAAE,0BAA0B,EAAE,MAAM,YAAY,CAAC;AACxD,cAAc,qBAAqB,CAAC;AACpC,OAAO,EAAE,2BAA2B,EAAE,MAAM,kBAAkB,CAAC;AAC/D,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,WAAW,CAAC;AAC1B,cAAc,qBAAqB,CAAC;AACpC,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,sBAAsB,EAA8B,MAAM,wBAAwB,CAAC;AAC5F,cAAc,YAAY,CAAC;AAC3B,OAAO,EACL,4BAA4B,GAG7B,MAAM,gBAAgB,CAAC;AACxB,cAAc,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client-cache/index.ts"],"names":[],"mappings":"AAAA,yFAAyF;AACzF,OAAO,EAAE,0BAA0B,EAAE,MAAM,YAAY,CAAC;AACxD,cAAc,qBAAqB,CAAC;AACpC,OAAO,EAAE,2BAA2B,EAAE,MAAM,kBAAkB,CAAC;AAC/D,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,WAAW,CAAC;AAC1B,cAAc,qBAAqB,CAAC;AACpC,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,sBAAsB,EAA8B,MAAM,wBAAwB,CAAC;AAC5F,cAAc,YAAY,CAAC;AAC3B,cAAc,oBAAoB,CAAC;AACnC,OAAO,EACL,4BAA4B,GAG7B,MAAM,gBAAgB,CAAC;AACxB,cAAc,YAAY,CAAC"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { NativeCommandReceipt, NativeSessionEvent, NativeSessionReadResult, NativeSessionTargetParams } from '../rpc/native-session.js';
|
|
1
|
+
import type { NativeCommandReceipt, NativeSessionEvent, NativeSessionReadResult, NativeSessionTargetParams, NativeSessionTool } from '../rpc/native-session.js';
|
|
2
2
|
import { type ResourceQuery } from './registry.js';
|
|
3
3
|
import type { ResourceQueryState, ResourceReadResult } from './types.js';
|
|
4
4
|
export interface NativeTimelineEntry {
|
|
5
5
|
key: number;
|
|
6
|
-
kind: 'user' | 'assistant' | 'tool' | 'status';
|
|
6
|
+
kind: 'user' | 'assistant' | 'tool' | 'resolution' | 'status';
|
|
7
7
|
event: NativeSessionEvent;
|
|
8
8
|
text: string;
|
|
9
9
|
}
|
|
@@ -13,6 +13,7 @@ export interface NativeHistorySnapshot extends NativeSessionReadResult {
|
|
|
13
13
|
historyRead: {
|
|
14
14
|
pageReadCount: number;
|
|
15
15
|
pageAfters: number[];
|
|
16
|
+
projection?: number;
|
|
16
17
|
};
|
|
17
18
|
}
|
|
18
19
|
export declare const NATIVE_HISTORY_MESSAGES = 200;
|
|
@@ -58,9 +59,13 @@ export declare function nativeHistoryParams(value: unknown): NativeSessionTarget
|
|
|
58
59
|
export declare function nativeHistoryStorageId(query: Pick<ResourceQuery, 'id' | 'method' | 'params'>): string;
|
|
59
60
|
export declare function storedNativeHistoryMatches(target: NativeSessionTargetParams, value: unknown): value is NativeSessionReadResult;
|
|
60
61
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
62
|
+
* Sending a message: current, complete history of the conversation on screen. A newer read of it,
|
|
63
|
+
* landing while a turn streams, does not refuse the message; a resumed generation, another worker
|
|
64
|
+
* lease or a released task scope does. Native actions do not reread first, so history restored from
|
|
65
|
+
* storage (no read stamp) waits for a live read.
|
|
63
66
|
*/
|
|
67
|
+
export declare function nativeHistoryCanSend(observation: ResourceQueryState<NativeSessionReadResult> | undefined, rendered: NativeSessionReadResult | undefined, connected: boolean, now?: number): boolean;
|
|
68
|
+
/** An answer or stop acts on the exact history snapshot on screen. */
|
|
64
69
|
export declare function nativeHistoryCanAct(observation: ResourceQueryState<NativeSessionReadResult> | undefined, rendered: NativeSessionReadResult | undefined, connected: boolean, now?: number): boolean;
|
|
65
70
|
export interface NativeTranscript {
|
|
66
71
|
events: NativeSessionEvent[];
|
|
@@ -68,10 +73,95 @@ export interface NativeTranscript {
|
|
|
68
73
|
}
|
|
69
74
|
/** Page sequence is authoritative. Ignore repeats; reject a gap rather than hide missing history. */
|
|
70
75
|
export declare function appendNativePage(previous: NativeTranscript, page: NativeSessionReadResult): NativeTranscript;
|
|
76
|
+
/**
|
|
77
|
+
* The composer's send action for a session in this state: "Send" when it is idle, "Queue message"
|
|
78
|
+
* for the next turn while a turn runs or waits on a request. Undefined: it takes no input now.
|
|
79
|
+
*/
|
|
80
|
+
export declare function nativeSendLabel(state: NativeSessionReadResult['session']['state'] | undefined): 'Send' | 'Queue message' | undefined;
|
|
71
81
|
export declare function nativeDeliveryLabel(receipt?: NativeCommandReceipt): string;
|
|
72
82
|
/** A confirmed replacement generation must not inherit the old browser input lock. */
|
|
73
83
|
export declare function nativeCommandLockSettled(command: {
|
|
74
84
|
commandId: string;
|
|
75
85
|
generation: string;
|
|
76
86
|
}, currentGeneration: string, receipt?: NativeCommandReceipt, events?: NativeSessionEvent[]): boolean;
|
|
87
|
+
export interface NativeFollow {
|
|
88
|
+
/** The reader is at the end, so new output stays in view. */
|
|
89
|
+
following: boolean;
|
|
90
|
+
/** Timeline entries added while the reader was away from the end. */
|
|
91
|
+
unseen: number;
|
|
92
|
+
}
|
|
93
|
+
/** Opening a conversation, or jumping to its latest entry, follows again. */
|
|
94
|
+
export declare const NATIVE_FOLLOW_LATEST: NativeFollow;
|
|
95
|
+
/** The reader scrolled. Back at the end means caught up. */
|
|
96
|
+
export declare function nativeFollowScrolled(state: NativeFollow, distanceFromEnd: number): NativeFollow;
|
|
97
|
+
/** Entries arrived. A reader away from the end has that many more to catch up on. */
|
|
98
|
+
export declare function nativeFollowGrew(state: NativeFollow, added: number): NativeFollow;
|
|
99
|
+
/** The jump control's label, or none while the reader follows. */
|
|
100
|
+
export declare function nativeFollowLabel(state: NativeFollow): string | undefined;
|
|
101
|
+
export interface NativeMessagePreview {
|
|
102
|
+
text: string;
|
|
103
|
+
lines: number;
|
|
104
|
+
}
|
|
105
|
+
/** A long message (a worker's dispatch prompt) shows its start until the reader expands it. */
|
|
106
|
+
export declare function nativeMessagePreview(text: string): NativeMessagePreview | undefined;
|
|
107
|
+
/** Only the reader's own messages collapse; assistant, tool and status entries show in full. */
|
|
108
|
+
export declare function nativeEntryPreview(entry: NativeTimelineEntry): NativeMessagePreview | undefined;
|
|
109
|
+
export declare function nativeMessageToggleLabel(preview: NativeMessagePreview, expanded: boolean): string;
|
|
110
|
+
export interface NativeWork {
|
|
111
|
+
/** When the open turn started, in ms. */
|
|
112
|
+
since: number;
|
|
113
|
+
/** The tool the turn is running now. */
|
|
114
|
+
tool?: string;
|
|
115
|
+
}
|
|
116
|
+
/** The turn in progress, if any: when it started and the tool it is running now. */
|
|
117
|
+
export declare function nativeWork(events: NativeSessionEvent[]): NativeWork | undefined;
|
|
118
|
+
/**
|
|
119
|
+
* "Working · 1m 12s · Bash": how long the turn has run, and its current tool.
|
|
120
|
+
* `since` is the execution host's clock: a client clock ahead of it overstates the time by the skew.
|
|
121
|
+
*/
|
|
122
|
+
export declare function nativeWorkLabel(work: NativeWork, now: number): string;
|
|
123
|
+
/**
|
|
124
|
+
* A tool call as a reader reads it: "Run npm test", "Change a.ts, b.ts", "Read a.ts", "Search src",
|
|
125
|
+
* at most 120 characters before an ellipsis. A tool whose adapter gave no kind or subject keeps the
|
|
126
|
+
* runner's own name.
|
|
127
|
+
*/
|
|
128
|
+
export declare function nativeToolLabel(tool: NativeSessionTool | undefined): string;
|
|
129
|
+
/**
|
|
130
|
+
* A command card's text: "$ npm test", then the last 10 lines it printed, with "… 12 earlier lines"
|
|
131
|
+
* above them; its raw input and output stay one tap away. Undefined for any other tool, whose card
|
|
132
|
+
* shows its raw input and output.
|
|
133
|
+
*/
|
|
134
|
+
export declare function nativeCommandText(tool: NativeSessionTool | undefined): string | undefined;
|
|
135
|
+
export type NativeTimelineRow =
|
|
136
|
+
/** `fold`: the summary row that hides this tool while folded. */
|
|
137
|
+
{
|
|
138
|
+
kind: 'entry';
|
|
139
|
+
key: number;
|
|
140
|
+
entry: NativeTimelineEntry;
|
|
141
|
+
fold?: number;
|
|
142
|
+
}
|
|
143
|
+
/** Keyed by the entry that ended its turn; `tools` are the keys it hides. */
|
|
144
|
+
| {
|
|
145
|
+
kind: 'work';
|
|
146
|
+
key: number;
|
|
147
|
+
label: string;
|
|
148
|
+
tools: number[];
|
|
149
|
+
};
|
|
150
|
+
/**
|
|
151
|
+
* The timeline with a summary row for each finished turn's tool calls, "Worked for 4m 12s ·
|
|
152
|
+
* 6 tools", just before the turn's end, and those tools marked with it. Nothing moves when a
|
|
153
|
+
* turn finishes: the tools keep their place and their keys, and the row is added below them.
|
|
154
|
+
* A turn ends at `turn.completed`, or at `session.closed` when its host stopped mid-turn.
|
|
155
|
+
* Messages and statuses stay; a running turn has no summary.
|
|
156
|
+
*/
|
|
157
|
+
export declare function nativeTimelineRows(entries: NativeTimelineEntry[]): NativeTimelineRow[];
|
|
158
|
+
/**
|
|
159
|
+
* Tools shown running in a live read here. A turn the reader watched starts open when it
|
|
160
|
+
* finishes, so its tools don't fold away under them. A stored snapshot is not live: a turn that
|
|
161
|
+
* finished while the reader was away starts folded.
|
|
162
|
+
*/
|
|
163
|
+
export declare function nativeWatchTools(rows: NativeTimelineRow[], watched: Set<number>, live: boolean): void;
|
|
164
|
+
export declare function nativeFoldWatched(row: Extract<NativeTimelineRow, {
|
|
165
|
+
kind: 'work';
|
|
166
|
+
}>, watched: ReadonlySet<number>): boolean;
|
|
77
167
|
//# sourceMappingURL=native-history.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native-history.d.ts","sourceRoot":"","sources":["../../src/client-cache/native-history.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,oBAAoB,EACpB,kBAAkB,EAClB,uBAAuB,
|
|
1
|
+
{"version":3,"file":"native-history.d.ts","sourceRoot":"","sources":["../../src/client-cache/native-history.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Methods } from '../rpc/registry.js';
|
|
2
2
|
import { canActOn } from './held-data.js';
|
|
3
3
|
import { resourceQuery } from './registry.js';
|
|
4
|
+
import { terminalLinesFromText } from './terminal-text.js';
|
|
4
5
|
export function nativeTimeline(events) {
|
|
5
6
|
const entries = [];
|
|
6
7
|
const userCommands = new Set();
|
|
@@ -37,27 +38,40 @@ export function nativeTimeline(events) {
|
|
|
37
38
|
if (previous) {
|
|
38
39
|
const completed = previous.event.type === 'tool.completed' ? previous.event : event;
|
|
39
40
|
const started = event.type === 'tool.started' ? event : previous.event;
|
|
41
|
+
// Keep the entry's sequence: cached history keeps only this merged event, and the next read
|
|
42
|
+
// keys the tool by it, so a card the reader opened stays the same card when it finishes.
|
|
40
43
|
previous.event = {
|
|
41
44
|
...completed,
|
|
45
|
+
sequence: previous.key,
|
|
42
46
|
tool: {
|
|
43
47
|
...started.tool,
|
|
44
48
|
...completed.tool,
|
|
45
49
|
name: started.tool?.name ?? completed.tool?.name ?? 'Tool',
|
|
46
50
|
},
|
|
47
51
|
};
|
|
52
|
+
// A runner can name the command or files only after the call started (Cursor does).
|
|
53
|
+
previous.text = nativeToolLabel(previous.event.tool);
|
|
48
54
|
}
|
|
49
55
|
else {
|
|
50
56
|
const entry = {
|
|
51
57
|
key: event.sequence,
|
|
52
58
|
kind: 'tool',
|
|
53
59
|
event,
|
|
54
|
-
text: event.tool
|
|
60
|
+
text: nativeToolLabel(event.tool),
|
|
55
61
|
};
|
|
56
62
|
entries.push(entry);
|
|
57
63
|
if (identity)
|
|
58
64
|
tools.set(identity, entry);
|
|
59
65
|
}
|
|
60
66
|
}
|
|
67
|
+
else if (event.type === 'approval.resolved' && event.request) {
|
|
68
|
+
entries.push({
|
|
69
|
+
key: event.sequence,
|
|
70
|
+
kind: 'resolution',
|
|
71
|
+
event,
|
|
72
|
+
text: nativeResolutionText(event.request, event.response),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
61
75
|
else if (['turn.completed', 'session.closed', 'error'].includes(event.type)) {
|
|
62
76
|
entries.push({
|
|
63
77
|
key: event.sequence,
|
|
@@ -74,6 +88,30 @@ export function nativeTimeline(events) {
|
|
|
74
88
|
}
|
|
75
89
|
return entries;
|
|
76
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* "Approved: rm -r build", "Declined: …", "Answered: Which file? → a.ts", or "Resolved: …" when
|
|
93
|
+
* the host did not deliver the response (the runner resolved it), so the decision is unknown.
|
|
94
|
+
*/
|
|
95
|
+
function nativeResolutionText(request, response) {
|
|
96
|
+
const questions = request.questions ?? [];
|
|
97
|
+
const answered = questions.flatMap((question) => {
|
|
98
|
+
const answer = response?.answers?.[question.id];
|
|
99
|
+
return answer?.length ? [`${question.prompt} → ${answer.join(', ')}`] : [];
|
|
100
|
+
});
|
|
101
|
+
if (answered.length)
|
|
102
|
+
return `Answered: ${answered.join('; ')}`;
|
|
103
|
+
const subject = questions.length
|
|
104
|
+
? questions.map((question) => question.prompt).join('; ')
|
|
105
|
+
: (request.detail ?? request.tool?.name ?? request.title);
|
|
106
|
+
const outcome = response?.decision === 'approve'
|
|
107
|
+
? 'Approved'
|
|
108
|
+
: response?.decision === 'deny'
|
|
109
|
+
? 'Declined'
|
|
110
|
+
: 'Resolved';
|
|
111
|
+
return `${outcome}: ${subject}`;
|
|
112
|
+
}
|
|
113
|
+
/** Bumped when retention keeps more: a snapshot saved by an older reader is read again from its start, once. */
|
|
114
|
+
const NATIVE_HISTORY_PROJECTION = 3;
|
|
77
115
|
export const NATIVE_HISTORY_MESSAGES = 200;
|
|
78
116
|
export const NATIVE_HISTORY_SESSIONS = 10;
|
|
79
117
|
/** One durable observation per opened session/binding, never per cursor page. */
|
|
@@ -99,10 +137,17 @@ export function retainNativeMessages(events) {
|
|
|
99
137
|
.reverse()
|
|
100
138
|
.filter((entry) => entry.kind !== 'status' || ++statuses <= NATIVE_HISTORY_MESSAGES)
|
|
101
139
|
.reverse();
|
|
102
|
-
|
|
140
|
+
const kept = retained.map((entry) => ({
|
|
103
141
|
...entry.event,
|
|
104
142
|
...(entry.kind === 'assistant' || entry.kind === 'user' ? { text: entry.text } : {}),
|
|
105
143
|
}));
|
|
144
|
+
// A running turn's start is not a timeline entry, but its working time is counted from it.
|
|
145
|
+
// Appended: the timeline skips it and nativeWork compares sequences, so its place does not
|
|
146
|
+
// matter, and sorting would move tool cards that finished out of order.
|
|
147
|
+
const turn = nativeOpenTurn(events);
|
|
148
|
+
if (!turn || kept.some((item) => item.sequence === turn.sequence))
|
|
149
|
+
return kept;
|
|
150
|
+
return [...kept, turn];
|
|
106
151
|
}
|
|
107
152
|
export function sameNativeHistoryIdentity(a, b) {
|
|
108
153
|
const identity = (page) => JSON.stringify([
|
|
@@ -142,11 +187,13 @@ function assertHistoryNotAborted(signal) {
|
|
|
142
187
|
* Cursor drains remain read-only. Neither receipts nor pending intents are dispatched here.
|
|
143
188
|
*/
|
|
144
189
|
export async function readNativeHistory(target, previous, request, signal) {
|
|
145
|
-
|
|
146
|
-
|
|
190
|
+
const previousRead = previous?.historyRead;
|
|
191
|
+
// Saved by a reader that dropped the running turn's start: keep its identity, reread its events.
|
|
192
|
+
const replay = previousRead !== undefined && previousRead.projection !== NATIVE_HISTORY_PROJECTION;
|
|
193
|
+
let retained = previous && replay ? { ...previous, events: [] } : previous;
|
|
194
|
+
let cursor = replay ? undefined : previous?.cursor;
|
|
147
195
|
let stamp;
|
|
148
196
|
let first = true;
|
|
149
|
-
const previousRead = previous?.historyRead;
|
|
150
197
|
let pageReadCount = previousRead?.pageReadCount ?? 0;
|
|
151
198
|
let pageAfters = previousRead?.pageAfters ?? [];
|
|
152
199
|
for (let pages = 0; pages < 1000; pages++) {
|
|
@@ -202,7 +249,10 @@ export async function readNativeHistory(target, previous, request, signal) {
|
|
|
202
249
|
cursor = page.cursor;
|
|
203
250
|
if (!page.hasMore)
|
|
204
251
|
return {
|
|
205
|
-
data: {
|
|
252
|
+
data: {
|
|
253
|
+
...retained,
|
|
254
|
+
historyRead: { pageReadCount, pageAfters, projection: NATIVE_HISTORY_PROJECTION },
|
|
255
|
+
},
|
|
206
256
|
resourceState: stamp,
|
|
207
257
|
};
|
|
208
258
|
}
|
|
@@ -276,16 +326,28 @@ export function storedNativeHistoryMatches(target, value) {
|
|
|
276
326
|
page.session.workerLeaseId === target.worker.leaseId &&
|
|
277
327
|
page.session.generation === target.worker.generation)));
|
|
278
328
|
}
|
|
279
|
-
/**
|
|
280
|
-
|
|
281
|
-
* not reread first, so history restored from storage (no read stamp) waits for a live read.
|
|
282
|
-
*/
|
|
283
|
-
export function nativeHistoryCanAct(observation, rendered, connected, now = Date.now()) {
|
|
329
|
+
/** Current, complete history under the shared held-data rule. */
|
|
330
|
+
function nativeHistoryCurrent(observation, connected, now) {
|
|
284
331
|
return (!!observation?.resourceState &&
|
|
285
332
|
canActOn(observation, connected, now) &&
|
|
333
|
+
!!observation.data &&
|
|
334
|
+
!observation.data.hasMore);
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Sending a message: current, complete history of the conversation on screen. A newer read of it,
|
|
338
|
+
* landing while a turn streams, does not refuse the message; a resumed generation, another worker
|
|
339
|
+
* lease or a released task scope does. Native actions do not reread first, so history restored from
|
|
340
|
+
* storage (no read stamp) waits for a live read.
|
|
341
|
+
*/
|
|
342
|
+
export function nativeHistoryCanSend(observation, rendered, connected, now = Date.now()) {
|
|
343
|
+
return (nativeHistoryCurrent(observation, connected, now) &&
|
|
286
344
|
!!rendered &&
|
|
287
|
-
observation.data
|
|
288
|
-
!
|
|
345
|
+
sameNativeHistoryIdentity(observation.data, rendered) &&
|
|
346
|
+
!observation.data.scope?.released);
|
|
347
|
+
}
|
|
348
|
+
/** An answer or stop acts on the exact history snapshot on screen. */
|
|
349
|
+
export function nativeHistoryCanAct(observation, rendered, connected, now = Date.now()) {
|
|
350
|
+
return nativeHistoryCurrent(observation, connected, now) && observation.data === rendered;
|
|
289
351
|
}
|
|
290
352
|
/** Page sequence is authoritative. Ignore repeats; reject a gap rather than hide missing history. */
|
|
291
353
|
export function appendNativePage(previous, page) {
|
|
@@ -303,6 +365,17 @@ export function appendNativePage(previous, page) {
|
|
|
303
365
|
}
|
|
304
366
|
return cursor === previous.cursor ? previous : { events, cursor };
|
|
305
367
|
}
|
|
368
|
+
/**
|
|
369
|
+
* The composer's send action for a session in this state: "Send" when it is idle, "Queue message"
|
|
370
|
+
* for the next turn while a turn runs or waits on a request. Undefined: it takes no input now.
|
|
371
|
+
*/
|
|
372
|
+
export function nativeSendLabel(state) {
|
|
373
|
+
if (state === 'idle')
|
|
374
|
+
return 'Send';
|
|
375
|
+
if (state === 'running' || state === 'waiting')
|
|
376
|
+
return 'Queue message';
|
|
377
|
+
return undefined;
|
|
378
|
+
}
|
|
306
379
|
export function nativeDeliveryLabel(receipt) {
|
|
307
380
|
if (!receipt)
|
|
308
381
|
return 'Delivery unknown';
|
|
@@ -334,4 +407,250 @@ export function nativeCommandLockSettled(command, currentGeneration, receipt, ev
|
|
|
334
407
|
event.commandId === command.commandId &&
|
|
335
408
|
event.generation === command.generation));
|
|
336
409
|
}
|
|
410
|
+
/** A reader within this distance of the end of a conversation follows new output. */
|
|
411
|
+
const NATIVE_FOLLOW_SLACK_PX = 80;
|
|
412
|
+
/** Opening a conversation, or jumping to its latest entry, follows again. */
|
|
413
|
+
export const NATIVE_FOLLOW_LATEST = { following: true, unseen: 0 };
|
|
414
|
+
/** The reader scrolled. Back at the end means caught up. */
|
|
415
|
+
export function nativeFollowScrolled(state, distanceFromEnd) {
|
|
416
|
+
if (distanceFromEnd < NATIVE_FOLLOW_SLACK_PX)
|
|
417
|
+
return NATIVE_FOLLOW_LATEST;
|
|
418
|
+
return state.following ? { following: false, unseen: 0 } : state;
|
|
419
|
+
}
|
|
420
|
+
/** Entries arrived. A reader away from the end has that many more to catch up on. */
|
|
421
|
+
export function nativeFollowGrew(state, added) {
|
|
422
|
+
return state.following || added <= 0 ? state : { ...state, unseen: state.unseen + added };
|
|
423
|
+
}
|
|
424
|
+
/** The jump control's label, or none while the reader follows. */
|
|
425
|
+
export function nativeFollowLabel(state) {
|
|
426
|
+
if (state.following)
|
|
427
|
+
return undefined;
|
|
428
|
+
return state.unseen ? `Latest · ${state.unseen} new` : 'Latest';
|
|
429
|
+
}
|
|
430
|
+
const NATIVE_MESSAGE_PREVIEW_LINES = 8;
|
|
431
|
+
const NATIVE_MESSAGE_PREVIEW_CHARS = 600;
|
|
432
|
+
// A message collapses only when that hides a real amount, never a line or a few characters.
|
|
433
|
+
const NATIVE_MESSAGE_COLLAPSE_LINES = 12;
|
|
434
|
+
const NATIVE_MESSAGE_COLLAPSE_CHARS = 900;
|
|
435
|
+
/** A long message (a worker's dispatch prompt) shows its start until the reader expands it. */
|
|
436
|
+
export function nativeMessagePreview(text) {
|
|
437
|
+
// A trailing newline is not a line the reader sees.
|
|
438
|
+
const body = text.replace(/\n+$/, '');
|
|
439
|
+
const lines = body.split('\n');
|
|
440
|
+
if (body.length <= NATIVE_MESSAGE_COLLAPSE_CHARS && lines.length <= NATIVE_MESSAGE_COLLAPSE_LINES)
|
|
441
|
+
return undefined;
|
|
442
|
+
const head = lines.slice(0, NATIVE_MESSAGE_PREVIEW_LINES).join('\n');
|
|
443
|
+
return { text: nativeClip(head, NATIVE_MESSAGE_PREVIEW_CHARS), lines: lines.length };
|
|
444
|
+
}
|
|
445
|
+
/** At most `max` characters, then "…" where it was cut, never splitting a surrogate pair. */
|
|
446
|
+
function nativeClip(text, max) {
|
|
447
|
+
if (text.length <= max)
|
|
448
|
+
return text;
|
|
449
|
+
return `${text
|
|
450
|
+
.slice(0, max)
|
|
451
|
+
.replace(/[\uD800-\uDBFF]$/, '')
|
|
452
|
+
.trimEnd()}…`;
|
|
453
|
+
}
|
|
454
|
+
/** Only the reader's own messages collapse; assistant, tool and status entries show in full. */
|
|
455
|
+
export function nativeEntryPreview(entry) {
|
|
456
|
+
return entry.kind === 'user' ? nativeMessagePreview(entry.text) : undefined;
|
|
457
|
+
}
|
|
458
|
+
export function nativeMessageToggleLabel(preview, expanded) {
|
|
459
|
+
if (expanded)
|
|
460
|
+
return 'Show less';
|
|
461
|
+
return `Show all · ${preview.lines} ${preview.lines === 1 ? 'line' : 'lines'}`;
|
|
462
|
+
}
|
|
463
|
+
/** The start of the turn still running, if any. A restarted host cannot continue a turn. */
|
|
464
|
+
function nativeOpenTurn(events) {
|
|
465
|
+
let turn;
|
|
466
|
+
for (const event of events) {
|
|
467
|
+
if (event.type === 'turn.started')
|
|
468
|
+
turn = event;
|
|
469
|
+
else if (['turn.completed', 'session.closed', 'session.started'].includes(event.type))
|
|
470
|
+
turn = undefined;
|
|
471
|
+
}
|
|
472
|
+
return turn;
|
|
473
|
+
}
|
|
474
|
+
/** The turn in progress, if any: when it started and the tool it is running now. */
|
|
475
|
+
export function nativeWork(events) {
|
|
476
|
+
const turn = nativeOpenTurn(events);
|
|
477
|
+
if (!turn)
|
|
478
|
+
return undefined;
|
|
479
|
+
const tools = new Map();
|
|
480
|
+
for (const event of events) {
|
|
481
|
+
if (event.sequence <= turn.sequence || !event.nativeId || !event.tool)
|
|
482
|
+
continue;
|
|
483
|
+
if (event.type === 'tool.started')
|
|
484
|
+
tools.set(event.nativeId, nativeToolLabel(event.tool));
|
|
485
|
+
else if (event.type === 'tool.completed')
|
|
486
|
+
tools.delete(event.nativeId);
|
|
487
|
+
}
|
|
488
|
+
return { since: Date.parse(turn.at), tool: [...tools.values()].at(-1) };
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* "Working · 1m 12s · Bash": how long the turn has run, and its current tool.
|
|
492
|
+
* `since` is the execution host's clock: a client clock ahead of it overstates the time by the skew.
|
|
493
|
+
*/
|
|
494
|
+
export function nativeWorkLabel(work, now) {
|
|
495
|
+
return `Working · ${nativeElapsed(now - work.since)}${work.tool ? ` · ${work.tool}` : ''}`;
|
|
496
|
+
}
|
|
497
|
+
/** "12s", "1m 12s", "1h 3m". */
|
|
498
|
+
function nativeElapsed(ms) {
|
|
499
|
+
const seconds = Math.max(0, Math.floor(ms / 1000));
|
|
500
|
+
const minutes = Math.floor(seconds / 60);
|
|
501
|
+
if (minutes >= 60)
|
|
502
|
+
return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
|
|
503
|
+
return minutes ? `${minutes}m ${seconds % 60}s` : `${seconds}s`;
|
|
504
|
+
}
|
|
505
|
+
const NATIVE_TOOL_LABEL_CHARS = 120;
|
|
506
|
+
const NATIVE_TOOL_LABEL_FILES = 3;
|
|
507
|
+
// Fits a tool card's box without scrolling, so the end of the output is what the reader sees.
|
|
508
|
+
const NATIVE_TOOL_OUTPUT_LINES = 10;
|
|
509
|
+
const NATIVE_COMMAND_CHARS = 200;
|
|
510
|
+
// "Change", not "Edit": the kind also covers creating, deleting and moving a file.
|
|
511
|
+
const NATIVE_TOOL_VERBS = {
|
|
512
|
+
command: 'Run',
|
|
513
|
+
'file-edit': 'Change',
|
|
514
|
+
'file-read': 'Read',
|
|
515
|
+
search: 'Search',
|
|
516
|
+
};
|
|
517
|
+
/**
|
|
518
|
+
* A tool call as a reader reads it: "Run npm test", "Change a.ts, b.ts", "Read a.ts", "Search src",
|
|
519
|
+
* at most 120 characters before an ellipsis. A tool whose adapter gave no kind or subject keeps the
|
|
520
|
+
* runner's own name.
|
|
521
|
+
*/
|
|
522
|
+
export function nativeToolLabel(tool) {
|
|
523
|
+
if (!tool)
|
|
524
|
+
return 'Tool';
|
|
525
|
+
const names = (tool.paths ?? []).map((path) => path.replace(/\/+$/, '').split('/').at(-1) || path);
|
|
526
|
+
const files = names.length > NATIVE_TOOL_LABEL_FILES
|
|
527
|
+
? `${names.slice(0, NATIVE_TOOL_LABEL_FILES).join(', ')} and ${names.length - NATIVE_TOOL_LABEL_FILES} more`
|
|
528
|
+
: names.join(', ');
|
|
529
|
+
const verb = tool.kind && NATIVE_TOOL_VERBS[tool.kind];
|
|
530
|
+
const subject = tool.kind === 'command' ? tool.command?.trim().split(/\r?\n/)[0] : files;
|
|
531
|
+
if (!verb || !subject)
|
|
532
|
+
return tool.name;
|
|
533
|
+
return nativeClip(`${verb} ${subject}`, NATIVE_TOOL_LABEL_CHARS);
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* A command card's text: "$ npm test", then the last 10 lines it printed, with "… 12 earlier lines"
|
|
537
|
+
* above them; its raw input and output stay one tap away. Undefined for any other tool, whose card
|
|
538
|
+
* shows its raw input and output.
|
|
539
|
+
*/
|
|
540
|
+
export function nativeCommandText(tool) {
|
|
541
|
+
if (tool?.kind !== 'command' || !tool.command)
|
|
542
|
+
return undefined;
|
|
543
|
+
// One line of the command: a heredoc would push the output out of view. All of it is in Raw.
|
|
544
|
+
const lines = tool.command.trim().split(/\r?\n/);
|
|
545
|
+
const command = `$ ${nativeClip(lines.length > 1 ? `${lines[0]} …` : lines[0], NATIVE_COMMAND_CHARS)}`;
|
|
546
|
+
if (tool.output === undefined)
|
|
547
|
+
return command;
|
|
548
|
+
const text = nativeOutputText(tool.output);
|
|
549
|
+
// Output that is not text would look like no output here; its raw card shows it.
|
|
550
|
+
if (text === undefined)
|
|
551
|
+
return undefined;
|
|
552
|
+
const output = terminalLinesFromText(text.replace(/[\r\n]+$/, ''));
|
|
553
|
+
if (!output.join(''))
|
|
554
|
+
return command;
|
|
555
|
+
const hidden = Math.max(0, output.length - NATIVE_TOOL_OUTPUT_LINES);
|
|
556
|
+
return [
|
|
557
|
+
command,
|
|
558
|
+
...(hidden ? [`… ${hidden} earlier ${hidden === 1 ? 'line' : 'lines'}`] : []),
|
|
559
|
+
...output.slice(hidden),
|
|
560
|
+
].join('\n');
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Runners return output as a string, as text blocks (`{ text }` or `{ content: { text } }`), or as a
|
|
564
|
+
* process result (`{ stdout, stderr }`).
|
|
565
|
+
*/
|
|
566
|
+
function nativeOutputText(output) {
|
|
567
|
+
if (typeof output === 'string')
|
|
568
|
+
return output;
|
|
569
|
+
if (output && typeof output === 'object' && !Array.isArray(output)) {
|
|
570
|
+
const { stdout, stderr } = output;
|
|
571
|
+
const streams = [stdout, stderr].filter((stream) => typeof stream === 'string');
|
|
572
|
+
if (!streams.length)
|
|
573
|
+
return undefined;
|
|
574
|
+
return streams
|
|
575
|
+
.map((stream) => stream.replace(/[\r\n]+$/, ''))
|
|
576
|
+
.filter(Boolean)
|
|
577
|
+
.join('\n');
|
|
578
|
+
}
|
|
579
|
+
if (!Array.isArray(output))
|
|
580
|
+
return undefined;
|
|
581
|
+
const texts = output.flatMap((block) => {
|
|
582
|
+
const value = block && typeof block === 'object' ? block : {};
|
|
583
|
+
const content = value.content && typeof value.content === 'object'
|
|
584
|
+
? value.content
|
|
585
|
+
: {};
|
|
586
|
+
const text = typeof value.text === 'string' ? value.text : content.text;
|
|
587
|
+
return typeof text === 'string' ? [text] : [];
|
|
588
|
+
});
|
|
589
|
+
return texts.length ? texts.join('\n') : undefined;
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* The timeline with a summary row for each finished turn's tool calls, "Worked for 4m 12s ·
|
|
593
|
+
* 6 tools", just before the turn's end, and those tools marked with it. Nothing moves when a
|
|
594
|
+
* turn finishes: the tools keep their place and their keys, and the row is added below them.
|
|
595
|
+
* A turn ends at `turn.completed`, or at `session.closed` when its host stopped mid-turn.
|
|
596
|
+
* Messages and statuses stay; a running turn has no summary.
|
|
597
|
+
*/
|
|
598
|
+
export function nativeTimelineRows(entries) {
|
|
599
|
+
const rows = [];
|
|
600
|
+
let turn = [];
|
|
601
|
+
for (const entry of entries) {
|
|
602
|
+
const ends = entry.kind === 'status' && ['turn.completed', 'session.closed'].includes(entry.event.type);
|
|
603
|
+
if (!ends) {
|
|
604
|
+
turn.push(entry);
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
const tools = turn.filter((item) => item.kind === 'tool');
|
|
608
|
+
for (const item of turn)
|
|
609
|
+
rows.push({
|
|
610
|
+
kind: 'entry',
|
|
611
|
+
key: item.key,
|
|
612
|
+
entry: item,
|
|
613
|
+
...(tools.length && item.kind === 'tool' ? { fold: entry.key } : {}),
|
|
614
|
+
});
|
|
615
|
+
if (tools.length) {
|
|
616
|
+
const prompt = turn.find((item) => item.kind === 'user' &&
|
|
617
|
+
entry.event.commandId !== undefined &&
|
|
618
|
+
item.event.commandId === entry.event.commandId) ?? turn.find((item) => item.kind === 'user');
|
|
619
|
+
// Without its prompt in the kept history, the turn's first kept entry is the best start.
|
|
620
|
+
const opened = prompt ?? turn[0];
|
|
621
|
+
const elapsed = nativeElapsed(Date.parse(entry.event.at) - Date.parse(opened.event.at));
|
|
622
|
+
const outcome = entry.event.type === 'session.closed' || entry.event.status === 'interrupted'
|
|
623
|
+
? 'Stopped after'
|
|
624
|
+
: entry.event.status === 'failed'
|
|
625
|
+
? 'Failed after'
|
|
626
|
+
: 'Worked for';
|
|
627
|
+
rows.push({
|
|
628
|
+
kind: 'work',
|
|
629
|
+
key: entry.key,
|
|
630
|
+
label: `${outcome} ${elapsed} · ${tools.length} ${tools.length === 1 ? 'tool' : 'tools'}`,
|
|
631
|
+
tools: tools.map((tool) => tool.key),
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
rows.push({ kind: 'entry', key: entry.key, entry });
|
|
635
|
+
turn = [];
|
|
636
|
+
}
|
|
637
|
+
for (const item of turn)
|
|
638
|
+
rows.push({ kind: 'entry', key: item.key, entry: item });
|
|
639
|
+
return rows;
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Tools shown running in a live read here. A turn the reader watched starts open when it
|
|
643
|
+
* finishes, so its tools don't fold away under them. A stored snapshot is not live: a turn that
|
|
644
|
+
* finished while the reader was away starts folded.
|
|
645
|
+
*/
|
|
646
|
+
export function nativeWatchTools(rows, watched, live) {
|
|
647
|
+
if (!live)
|
|
648
|
+
return;
|
|
649
|
+
for (const row of rows)
|
|
650
|
+
if (row.kind === 'entry' && row.entry.kind === 'tool' && row.fold === undefined)
|
|
651
|
+
watched.add(row.key);
|
|
652
|
+
}
|
|
653
|
+
export function nativeFoldWatched(row, watched) {
|
|
654
|
+
return row.tools.some((key) => watched.has(key));
|
|
655
|
+
}
|
|
337
656
|
//# sourceMappingURL=native-history.js.map
|