@khorsheed/dsh-context-lens 0.0.0-stage → 0.1.0
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 +16 -0
- package/README.en.md +53 -0
- package/README.i18n.yaml +6 -0
- package/README.md +52 -2
- package/cordis.patch.yml +13 -0
- package/lib/client.js +26953 -0
- package/lib/index.js +3943 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +1569 -0
- package/lib/typert.remote-client.d.ts +40 -0
- package/lib/typert.remote-client.js +1202 -0
- package/lib/types/changes.d.ts +72 -0
- package/lib/types/changes.js +328 -0
- package/lib/types/client/AgentTabs.d.ts +34 -0
- package/lib/types/client/AgentTabs.js +89 -0
- package/lib/types/client/ChangeList.d.ts +28 -0
- package/lib/types/client/ChangeList.js +109 -0
- package/lib/types/client/ConfigFacts.d.ts +21 -0
- package/lib/types/client/ConfigFacts.js +30 -0
- package/lib/types/client/HeaderUtility.d.ts +12 -0
- package/lib/types/client/HeaderUtility.js +8 -0
- package/lib/types/client/InspectorPanel.d.ts +5 -0
- package/lib/types/client/InspectorPanel.js +486 -0
- package/lib/types/client/RawView.d.ts +18 -0
- package/lib/types/client/RawView.js +8 -0
- package/lib/types/client/RequestFacts.d.ts +19 -0
- package/lib/types/client/RequestFacts.js +39 -0
- package/lib/types/client/RoomDecisionsView.d.ts +24 -0
- package/lib/types/client/RoomDecisionsView.js +52 -0
- package/lib/types/client/SchemaFields.d.ts +13 -0
- package/lib/types/client/SchemaFields.js +96 -0
- package/lib/types/client/SegCtl.d.ts +32 -0
- package/lib/types/client/SegCtl.js +12 -0
- package/lib/types/client/SegmentBlocks.d.ts +28 -0
- package/lib/types/client/SegmentBlocks.js +225 -0
- package/lib/types/client/TimingView.d.ts +17 -0
- package/lib/types/client/TimingView.js +98 -0
- package/lib/types/client/TokenView.d.ts +21 -0
- package/lib/types/client/TokenView.js +22 -0
- package/lib/types/client/ToolCatalog.d.ts +9 -0
- package/lib/types/client/ToolCatalog.js +51 -0
- package/lib/types/client/Trend.d.ts +25 -0
- package/lib/types/client/Trend.js +100 -0
- package/lib/types/client/TurnAction.d.ts +14 -0
- package/lib/types/client/TurnAction.js +31 -0
- package/lib/types/client/TurnMenu.d.ts +36 -0
- package/lib/types/client/TurnMenu.js +50 -0
- package/lib/types/client/aggregate.d.ts +141 -0
- package/lib/types/client/aggregate.js +213 -0
- package/lib/types/client/contract.d.ts +58 -0
- package/lib/types/client/contract.js +1 -0
- package/lib/types/client/definition.d.ts +47 -0
- package/lib/types/client/definition.js +25 -0
- package/lib/types/client/icons-local.d.ts +19 -0
- package/lib/types/client/icons-local.js +3 -0
- package/lib/types/client/icons.d.ts +18 -0
- package/lib/types/client/icons.js +2 -0
- package/lib/types/client/index.d.ts +42 -0
- package/lib/types/client/index.js +139 -0
- package/lib/types/client/locales.d.ts +365 -0
- package/lib/types/client/locales.js +705 -0
- package/lib/types/client/preset-visibility.d.ts +99 -0
- package/lib/types/client/preset-visibility.js +108 -0
- package/lib/types/client/scroll-sync.d.ts +54 -0
- package/lib/types/client/scroll-sync.js +118 -0
- package/lib/types/client/timing.d.ts +29 -0
- package/lib/types/client/timing.js +117 -0
- package/lib/types/client/wire.d.ts +13 -0
- package/lib/types/client/wire.js +11 -0
- package/lib/types/index.d.ts +32 -0
- package/lib/types/index.js +25 -0
- package/lib/types/log-lane.d.ts +198 -0
- package/lib/types/log-lane.js +841 -0
- package/lib/types/messages.d.ts +96 -0
- package/lib/types/messages.js +254 -0
- package/lib/types/prefix-diff.d.ts +4 -0
- package/lib/types/prefix-diff.js +20 -0
- package/lib/types/remote.d.ts +45 -0
- package/lib/types/remote.js +99 -0
- package/lib/types/room.d.ts +66 -0
- package/lib/types/room.js +101 -0
- package/lib/types/segmenter.d.ts +100 -0
- package/lib/types/segmenter.js +184 -0
- package/lib/types/service.d.ts +145 -0
- package/lib/types/service.js +493 -0
- package/lib/types/store.d.ts +424 -0
- package/lib/types/store.js +1920 -0
- package/lib/types/tool-summary.d.ts +6 -0
- package/lib/types/tool-summary.js +7 -0
- package/lib/types/types.d.ts +703 -0
- package/lib/types/types.js +18 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +133 -3
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { RequestChange, SegmentItem, ToolChange, ToolDefItem } from './types.ts';
|
|
2
|
+
/** One surface replacement as the host logged it. */
|
|
3
|
+
export interface ReplacementFact {
|
|
4
|
+
/** The replacing message's id (a prune keeps the shadowed id). */
|
|
5
|
+
newId: string;
|
|
6
|
+
/** Ids of the shadowed surface messages, in surface order. */
|
|
7
|
+
replacedIds: string[];
|
|
8
|
+
kind: 'replace' | 'compaction' | 'prune';
|
|
9
|
+
/** The replacing message's producer, verbatim. */
|
|
10
|
+
sourceKind: string;
|
|
11
|
+
sourceOp?: string;
|
|
12
|
+
/** Host log seq of the replacement event and of what it shadowed. */
|
|
13
|
+
eventSeq?: number;
|
|
14
|
+
replacedSeqs?: number[];
|
|
15
|
+
compactionId?: string;
|
|
16
|
+
}
|
|
17
|
+
/** Structural minimum of one session event / log row this module reads. */
|
|
18
|
+
export interface ReplacementRow {
|
|
19
|
+
type: string;
|
|
20
|
+
seq?: number;
|
|
21
|
+
data?: unknown;
|
|
22
|
+
surfaceOp?: unknown;
|
|
23
|
+
sourceEventSeqs?: readonly number[];
|
|
24
|
+
}
|
|
25
|
+
/** The metering event a replacement immediately follows, when any. */
|
|
26
|
+
export interface PendingMeter {
|
|
27
|
+
kind: 'compaction' | 'prune';
|
|
28
|
+
compactionId?: string;
|
|
29
|
+
}
|
|
30
|
+
/** Read the meter a `compaction/*` row announces for the next replacement. */
|
|
31
|
+
export declare function meterOf(row: ReplacementRow): PendingMeter | undefined;
|
|
32
|
+
/** The message a surface event carries: `data.message` for step events, `data` itself for user messages. */
|
|
33
|
+
export declare function messageOfRow(row: ReplacementRow): {
|
|
34
|
+
id?: string;
|
|
35
|
+
source?: Record<string, unknown>;
|
|
36
|
+
} | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* The fact one replacement row records, or undefined for appends and rows
|
|
39
|
+
* whose ids cannot be read.
|
|
40
|
+
* @param idAt - resolve a shadowed log seq to its message id.
|
|
41
|
+
* @param meter - the metering row immediately before this one, if any.
|
|
42
|
+
*/
|
|
43
|
+
export declare function replacementFact(row: ReplacementRow, idAt: (seq: number) => string | undefined, meter?: PendingMeter): ReplacementFact | undefined;
|
|
44
|
+
/** Every replacement fact of one whole log (the backfill path). */
|
|
45
|
+
export declare function replacementFactsOf(rows: readonly ReplacementRow[]): ReplacementFact[];
|
|
46
|
+
/** Fact lookup the diff needs; the store's bounded registry implements it. */
|
|
47
|
+
export interface FactLookup {
|
|
48
|
+
byNewId(id: string): ReplacementFact | undefined;
|
|
49
|
+
byReplacedId(id: string): ReplacementFact | undefined;
|
|
50
|
+
}
|
|
51
|
+
/** The comparable face of one request. */
|
|
52
|
+
export interface RequestFace {
|
|
53
|
+
rows: readonly SegmentItem[];
|
|
54
|
+
tools: readonly ToolDefItem[];
|
|
55
|
+
}
|
|
56
|
+
/** Result of {@link diffRequests}. */
|
|
57
|
+
export interface RequestChanges {
|
|
58
|
+
changes: RequestChange[];
|
|
59
|
+
tools: ToolChange[];
|
|
60
|
+
tail?: {
|
|
61
|
+
count: number;
|
|
62
|
+
sources: string[];
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Diff two consecutive requests.
|
|
67
|
+
* @param breakIndex - the message index where the exact prefix comparison first differs, if it does.
|
|
68
|
+
*/
|
|
69
|
+
export declare function diffRequests(previous: RequestFace, current: RequestFace, facts: FactLookup, breakIndex?: number): RequestChanges;
|
|
70
|
+
/** Tool definitions pair by name; description and parameters diff separately. */
|
|
71
|
+
export declare function diffTools(previous: readonly ToolDefItem[], current: readonly ToolDefItem[]): ToolChange[];
|
|
72
|
+
//# sourceMappingURL=changes.d.ts.map
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The change list between two consecutive requests of one scope. Every entry
|
|
3
|
+
* is a fact the host recorded or an exact comparison — never an inference:
|
|
4
|
+
*
|
|
5
|
+
* - Messages pair by durable id (content hash for id-less ones): ids only in
|
|
6
|
+
* the previous request were removed, ids only in the current one were added,
|
|
7
|
+
* a shared id whose text differs was rewritten in place.
|
|
8
|
+
* - A surface replacement event (`surfaceOp: replace`) names the ids it
|
|
9
|
+
* shadowed and the id it put in their place; that pairing — and its
|
|
10
|
+
* producer's `kind · op` — comes from the {@link ReplacementFact} registry,
|
|
11
|
+
* fed by the live `session/event` lane and by log backfill alike. The
|
|
12
|
+
* metering event the host appends right before a replacement
|
|
13
|
+
* (`compaction/summary`, `compaction/prune`) types it as a compaction or a
|
|
14
|
+
* prune. Without a fact the same rows stay an honest remove + add.
|
|
15
|
+
* - System rows group into one entry (the user's question is "did the system
|
|
16
|
+
* prompt change"), diffed head against head, with a checkable equality when
|
|
17
|
+
* the new head repeats a removed in-history system update verbatim.
|
|
18
|
+
* - Unexplained additions after the last kept row are the ordinary tail
|
|
19
|
+
* append, summarized rather than listed.
|
|
20
|
+
*
|
|
21
|
+
* @module @khorsheed/dsh-context-lens/changes
|
|
22
|
+
*/
|
|
23
|
+
import { prefixTextChange } from "./prefix-diff.js";
|
|
24
|
+
/** Read the meter a `compaction/*` row announces for the next replacement. */
|
|
25
|
+
export function meterOf(row) {
|
|
26
|
+
if (row.type === 'compaction/prune')
|
|
27
|
+
return { kind: 'prune' };
|
|
28
|
+
if (row.type !== 'compaction/summary')
|
|
29
|
+
return undefined;
|
|
30
|
+
const id = record(row.data)?.['compactionId'];
|
|
31
|
+
return { kind: 'compaction', ...typeof id === 'string' ? { compactionId: id } : {} };
|
|
32
|
+
}
|
|
33
|
+
function record(value) {
|
|
34
|
+
return typeof value === 'object' && value !== null ? value : undefined;
|
|
35
|
+
}
|
|
36
|
+
/** The message a surface event carries: `data.message` for step events, `data` itself for user messages. */
|
|
37
|
+
export function messageOfRow(row) {
|
|
38
|
+
const data = record(row.data);
|
|
39
|
+
if (data === undefined)
|
|
40
|
+
return undefined;
|
|
41
|
+
const message = record(data['message']) ?? data;
|
|
42
|
+
const id = message['id'];
|
|
43
|
+
const source = record(message['source']);
|
|
44
|
+
return { ...typeof id === 'string' ? { id } : {}, ...source === undefined ? {} : { source } };
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The fact one replacement row records, or undefined for appends and rows
|
|
48
|
+
* whose ids cannot be read.
|
|
49
|
+
* @param idAt - resolve a shadowed log seq to its message id.
|
|
50
|
+
* @param meter - the metering row immediately before this one, if any.
|
|
51
|
+
*/
|
|
52
|
+
export function replacementFact(row, idAt, meter) {
|
|
53
|
+
const op = record(row.surfaceOp);
|
|
54
|
+
if (op === undefined || op['op'] !== 'replace')
|
|
55
|
+
return undefined;
|
|
56
|
+
const message = messageOfRow(row);
|
|
57
|
+
if (message?.id === undefined)
|
|
58
|
+
return undefined;
|
|
59
|
+
const seqs = row.sourceEventSeqs !== undefined && row.sourceEventSeqs.length > 0
|
|
60
|
+
? [...row.sourceEventSeqs]
|
|
61
|
+
: typeof op['startSeq'] === 'number' && typeof op['endSeq'] === 'number' && op['endSeq'] >= op['startSeq']
|
|
62
|
+
? Array.from({ length: op['endSeq'] - op['startSeq'] + 1 }, (_, i) => op['startSeq'] + i)
|
|
63
|
+
: [];
|
|
64
|
+
const replacedIds = seqs.map(idAt).filter((id) => id !== undefined);
|
|
65
|
+
const sourceKind = typeof message.source?.['kind'] === 'string' ? message.source['kind'] : row.type;
|
|
66
|
+
const sourceOp = typeof message.source?.['op'] === 'string' ? message.source['op'] : undefined;
|
|
67
|
+
const compactionId = typeof message.source?.['compactionId'] === 'string' ? message.source['compactionId'] : meter?.compactionId;
|
|
68
|
+
const kind = meter?.kind ?? (compactionId !== undefined ? 'compaction' : 'replace');
|
|
69
|
+
return {
|
|
70
|
+
newId: message.id,
|
|
71
|
+
replacedIds,
|
|
72
|
+
kind,
|
|
73
|
+
sourceKind,
|
|
74
|
+
...sourceOp === undefined ? {} : { sourceOp },
|
|
75
|
+
...row.seq === undefined ? {} : { eventSeq: row.seq },
|
|
76
|
+
...seqs.length === 0 ? {} : { replacedSeqs: seqs },
|
|
77
|
+
...kind === 'compaction' && compactionId !== undefined ? { compactionId } : {},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** Every replacement fact of one whole log (the backfill path). */
|
|
81
|
+
export function replacementFactsOf(rows) {
|
|
82
|
+
const ids = new Map();
|
|
83
|
+
const facts = [];
|
|
84
|
+
let meter;
|
|
85
|
+
for (const row of rows) {
|
|
86
|
+
const fact = replacementFact(row, seq => ids.get(seq), meter);
|
|
87
|
+
if (fact !== undefined)
|
|
88
|
+
facts.push(fact);
|
|
89
|
+
const id = messageOfRow(row)?.id;
|
|
90
|
+
if (row.seq !== undefined && id !== undefined)
|
|
91
|
+
ids.set(row.seq, id);
|
|
92
|
+
meter = meterOf(row);
|
|
93
|
+
}
|
|
94
|
+
return facts;
|
|
95
|
+
}
|
|
96
|
+
function messagesOf(rows) {
|
|
97
|
+
const byIndex = new Map();
|
|
98
|
+
for (const row of rows)
|
|
99
|
+
byIndex.set(row.index, [...byIndex.get(row.index) ?? [], row]);
|
|
100
|
+
const seen = new Map();
|
|
101
|
+
return [...byIndex.entries()].sort((a, b) => a[0] - b[0]).map(([index, parts]) => {
|
|
102
|
+
const first = parts[0];
|
|
103
|
+
const sectioned = parts.length > 1;
|
|
104
|
+
const sections = [];
|
|
105
|
+
let cursor = 0;
|
|
106
|
+
for (const part of parts) {
|
|
107
|
+
sections.push({ name: part.source, start: cursor, end: cursor + Array.from(part.full).length });
|
|
108
|
+
cursor += Array.from(part.full).length + 2;
|
|
109
|
+
}
|
|
110
|
+
const text = parts.map(part => part.full).join('\n\n');
|
|
111
|
+
const id = parts.find(part => part.messageId !== undefined)?.messageId;
|
|
112
|
+
const base = id === undefined ? `h:${first.category}:${text.length}:${hashText(text)}` : `id:${id}`;
|
|
113
|
+
const n = seen.get(base) ?? 0;
|
|
114
|
+
seen.set(base, n + 1);
|
|
115
|
+
return {
|
|
116
|
+
index,
|
|
117
|
+
key: n === 0 ? base : `${base}#${n}`,
|
|
118
|
+
...id === undefined ? {} : { id },
|
|
119
|
+
category: first.category,
|
|
120
|
+
source: first.category === 'system' && sectioned ? 'system-prompt' : first.source,
|
|
121
|
+
text,
|
|
122
|
+
sections: sectioned ? sections : [],
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
function hashText(text) {
|
|
127
|
+
let hash = 0x811c9dc5;
|
|
128
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
129
|
+
hash ^= text.charCodeAt(i);
|
|
130
|
+
hash = Math.imul(hash, 0x01000193);
|
|
131
|
+
}
|
|
132
|
+
return (hash >>> 0).toString(36);
|
|
133
|
+
}
|
|
134
|
+
function rowOf(message) {
|
|
135
|
+
return {
|
|
136
|
+
index: message.index,
|
|
137
|
+
...message.id === undefined ? {} : { messageId: message.id },
|
|
138
|
+
category: message.category,
|
|
139
|
+
source: message.source,
|
|
140
|
+
preview: message.text.replace(/\s+/g, ' ').trim().slice(0, 120),
|
|
141
|
+
chars: Array.from(message.text).length,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function sectionAt(message, offset) {
|
|
145
|
+
return message.sections.find(section => offset >= section.start && offset <= section.end)?.name;
|
|
146
|
+
}
|
|
147
|
+
/** Contiguous runs (by index) of rows; system and non-system rows never share a run. */
|
|
148
|
+
function runs(messages) {
|
|
149
|
+
const out = [];
|
|
150
|
+
for (const message of messages) {
|
|
151
|
+
const last = out[out.length - 1];
|
|
152
|
+
const tail = last?.[last.length - 1];
|
|
153
|
+
if (last !== undefined && tail !== undefined && message.index === tail.index + 1 && (message.category === 'system') === (tail.category === 'system'))
|
|
154
|
+
last.push(message);
|
|
155
|
+
else
|
|
156
|
+
out.push([message]);
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Diff two consecutive requests.
|
|
162
|
+
* @param breakIndex - the message index where the exact prefix comparison first differs, if it does.
|
|
163
|
+
*/
|
|
164
|
+
export function diffRequests(previous, current, facts, breakIndex) {
|
|
165
|
+
const prev = messagesOf(previous.rows), cur = messagesOf(current.rows);
|
|
166
|
+
const prevByKey = new Map(prev.map(m => [m.key, m])), curByKey = new Map(cur.map(m => [m.key, m]));
|
|
167
|
+
const removed = prev.filter(m => !curByKey.has(m.key));
|
|
168
|
+
const added = cur.filter(m => !prevByKey.has(m.key));
|
|
169
|
+
const rewritten = cur.filter(m => m.id !== undefined && prevByKey.has(m.key) && prevByKey.get(m.key).text !== m.text);
|
|
170
|
+
// Resolve a replacement's shadowed ids back to ids the previous request carried (replace chains).
|
|
171
|
+
const resolve = (ids, depth = 0) => ids.flatMap(id => {
|
|
172
|
+
if (prev.some(m => m.id === id) || depth > 8)
|
|
173
|
+
return [id];
|
|
174
|
+
const inner = facts.byNewId(id);
|
|
175
|
+
return inner === undefined || inner.newId === id ? [id] : resolve(inner.replacedIds, depth + 1);
|
|
176
|
+
});
|
|
177
|
+
const explained = new Set();
|
|
178
|
+
const changes = [];
|
|
179
|
+
const seenFacts = new Set();
|
|
180
|
+
const candidates = [];
|
|
181
|
+
const want = (fact) => {
|
|
182
|
+
if (fact === undefined || seenFacts.has(fact.newId))
|
|
183
|
+
return;
|
|
184
|
+
seenFacts.add(fact.newId);
|
|
185
|
+
candidates.push(fact);
|
|
186
|
+
};
|
|
187
|
+
for (const m of added)
|
|
188
|
+
if (m.id !== undefined)
|
|
189
|
+
want(facts.byNewId(m.id));
|
|
190
|
+
for (const m of rewritten)
|
|
191
|
+
want(facts.byNewId(m.id));
|
|
192
|
+
for (const m of removed)
|
|
193
|
+
if (m.id !== undefined)
|
|
194
|
+
want(facts.byReplacedId(m.id));
|
|
195
|
+
for (const fact of candidates) {
|
|
196
|
+
const shadowed = new Set(resolve(fact.replacedIds));
|
|
197
|
+
const gone = removed.filter(m => m.id !== undefined && shadowed.has(m.id) && !explained.has(m.key));
|
|
198
|
+
const fresh = cur.filter(m => m.id === fact.newId);
|
|
199
|
+
const inPlace = fresh.filter(m => prevByKey.has(m.key));
|
|
200
|
+
const freshAdded = fresh.filter(m => !prevByKey.has(m.key));
|
|
201
|
+
if (gone.length === 0 && freshAdded.length === 0 && inPlace.length === 0)
|
|
202
|
+
continue;
|
|
203
|
+
for (const m of [...gone, ...freshAdded, ...inPlace])
|
|
204
|
+
explained.add(m.key);
|
|
205
|
+
const before = inPlace.length > 0 ? prevByKey.get(inPlace[0].key) : gone.length === 1 ? gone[0] : undefined;
|
|
206
|
+
const after = inPlace[0] ?? freshAdded[0];
|
|
207
|
+
const text = before !== undefined && after !== undefined ? prefixTextChange(before.text, after.text) : undefined;
|
|
208
|
+
changes.push({
|
|
209
|
+
kind: fact.kind,
|
|
210
|
+
removed: inPlace.length > 0 ? inPlace.map(m => rowOf(prevByKey.get(m.key))) : gone.map(rowOf),
|
|
211
|
+
added: [...inPlace, ...freshAdded].map(rowOf),
|
|
212
|
+
source: { kind: fact.sourceKind, ...fact.sourceOp === undefined ? {} : { op: fact.sourceOp } },
|
|
213
|
+
...fact.eventSeq === undefined ? {} : { eventSeq: fact.eventSeq },
|
|
214
|
+
...fact.replacedSeqs === undefined ? {} : { replacedSeqs: fact.replacedSeqs },
|
|
215
|
+
...fact.compactionId === undefined ? {} : { compactionId: fact.compactionId },
|
|
216
|
+
...text === undefined ? {} : { text },
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
// Same id, new text, no fact: an in-place rewrite the host did not label.
|
|
220
|
+
for (const m of rewritten) {
|
|
221
|
+
if (explained.has(m.key))
|
|
222
|
+
continue;
|
|
223
|
+
explained.add(m.key);
|
|
224
|
+
const before = prevByKey.get(m.key);
|
|
225
|
+
changes.push({ kind: 'replace', removed: [rowOf(before)], added: [rowOf(m)], text: prefixTextChange(before.text, m.text) });
|
|
226
|
+
}
|
|
227
|
+
const keptPrev = prev.filter(m => curByKey.has(m.key));
|
|
228
|
+
const keptCur = cur.filter(m => prevByKey.has(m.key));
|
|
229
|
+
const lastKeptPrev = Math.max(-1, ...keptPrev.map(m => m.index));
|
|
230
|
+
const anchor = Math.max(-1, ...keptCur.map(m => m.index), ...cur.filter(m => explained.has(m.key)).map(m => m.index));
|
|
231
|
+
const looseRemoved = removed.filter(m => !explained.has(m.key));
|
|
232
|
+
const looseAdded = added.filter(m => !explained.has(m.key));
|
|
233
|
+
const trailingRemovals = looseRemoved.some(m => m.index > lastKeptPrev);
|
|
234
|
+
// System rows never fold into the tail: a prompt update appended mid-history is what this list is for.
|
|
235
|
+
const tailRows = trailingRemovals ? [] : looseAdded.filter(m => m.index > anchor && m.category !== 'system');
|
|
236
|
+
const tailKeys = new Set(tailRows.map(m => m.key));
|
|
237
|
+
for (const run of runs(looseRemoved))
|
|
238
|
+
changes.push({ kind: 'remove', removed: run.map(rowOf), added: [] });
|
|
239
|
+
for (const run of runs(looseAdded.filter(m => !tailKeys.has(m.key))))
|
|
240
|
+
changes.push({ kind: 'add', removed: [], added: run.map(rowOf) });
|
|
241
|
+
const grouped = groupSystem(changes, prev, cur);
|
|
242
|
+
grouped.sort((a, b) => firstIndex(a) - firstIndex(b));
|
|
243
|
+
const tools = diffTools(previous.tools, current.tools);
|
|
244
|
+
const breakAt = breakIndex === undefined ? undefined
|
|
245
|
+
: grouped.find(c => c.added.some(r => r.index === breakIndex)) ?? grouped.find(c => c.removed.some(r => r.index === breakIndex));
|
|
246
|
+
if (breakAt !== undefined)
|
|
247
|
+
breakAt.isBreak = true;
|
|
248
|
+
else if (breakIndex === undefined && tools[0] !== undefined)
|
|
249
|
+
tools[0].isBreak = true;
|
|
250
|
+
return {
|
|
251
|
+
changes: grouped,
|
|
252
|
+
tools,
|
|
253
|
+
...tailRows.length === 0 ? {} : { tail: { count: tailRows.length, sources: [...new Set(tailRows.map(m => m.source))] } },
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
function firstIndex(change) {
|
|
257
|
+
return Math.min(...change.added.map(r => r.index), ...change.removed.map(r => r.index));
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Fold every all-system change into one entry. The text diff compares the
|
|
261
|
+
* head system message of both requests (the cache prefix); when the new head
|
|
262
|
+
* repeats a removed system message verbatim, that equality is recorded.
|
|
263
|
+
*/
|
|
264
|
+
function groupSystem(changes, prev, cur) {
|
|
265
|
+
const isSystem = (c) => [...c.removed, ...c.added].length > 0 && [...c.removed, ...c.added].every(r => r.category === 'system');
|
|
266
|
+
const system = changes.filter(isSystem);
|
|
267
|
+
if (system.length === 0)
|
|
268
|
+
return changes;
|
|
269
|
+
const rest = changes.filter(c => !isSystem(c));
|
|
270
|
+
const prevHead = prev.find(m => m.category === 'system'), curHead = cur.find(m => m.category === 'system');
|
|
271
|
+
let text;
|
|
272
|
+
let section;
|
|
273
|
+
if (prevHead !== undefined && curHead !== undefined && prevHead.index === curHead.index && prevHead.text !== curHead.text) {
|
|
274
|
+
text = prefixTextChange(prevHead.text, curHead.text);
|
|
275
|
+
section = sectionAt(curHead, text.offset);
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
// Head untouched: the update landed elsewhere; diff the effective (last non-empty) prompts.
|
|
279
|
+
const effective = (list) => list.filter(m => m.category === 'system' && m.text.length > 0).at(-1);
|
|
280
|
+
const before = effective(prev), after = effective(cur);
|
|
281
|
+
if (before !== undefined && after !== undefined && before.text !== after.text)
|
|
282
|
+
text = prefixTextChange(before.text, after.text);
|
|
283
|
+
}
|
|
284
|
+
const removed = system.flatMap(c => c.removed);
|
|
285
|
+
const sameAs = curHead === undefined ? undefined
|
|
286
|
+
: removed.find(r => r.index !== curHead.index && prev.find(m => m.index === r.index)?.text === curHead.text);
|
|
287
|
+
const anyFact = system.some(c => c.kind !== 'add' && c.kind !== 'remove');
|
|
288
|
+
const merged = {
|
|
289
|
+
kind: anyFact ? 'replace' : removed.length > 0 ? 'remove' : 'add',
|
|
290
|
+
system: true,
|
|
291
|
+
removed,
|
|
292
|
+
added: system.flatMap(c => c.added),
|
|
293
|
+
source: { kind: 'system-prompt' },
|
|
294
|
+
parts: system.map(c => ({
|
|
295
|
+
kind: c.kind,
|
|
296
|
+
removed: c.removed,
|
|
297
|
+
added: c.added,
|
|
298
|
+
...c.eventSeq === undefined ? {} : { eventSeq: c.eventSeq },
|
|
299
|
+
...c.replacedSeqs === undefined ? {} : { replacedSeqs: c.replacedSeqs },
|
|
300
|
+
})),
|
|
301
|
+
...text === undefined ? {} : { text },
|
|
302
|
+
...section === undefined ? {} : { section },
|
|
303
|
+
...sameAs === undefined ? {} : { sameAs },
|
|
304
|
+
};
|
|
305
|
+
return [merged, ...rest];
|
|
306
|
+
}
|
|
307
|
+
/** Tool definitions pair by name; description and parameters diff separately. */
|
|
308
|
+
export function diffTools(previous, current) {
|
|
309
|
+
const before = new Map(previous.map(tool => [tool.name, tool]));
|
|
310
|
+
const after = new Set(current.map(tool => tool.name));
|
|
311
|
+
const out = [];
|
|
312
|
+
for (const tool of current) {
|
|
313
|
+
const old = before.get(tool.name);
|
|
314
|
+
if (old === undefined) {
|
|
315
|
+
out.push({ name: tool.name, change: 'added' });
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
const description = old.description === tool.description ? undefined : prefixTextChange(old.description, tool.description);
|
|
319
|
+
const parameters = old.parameters === tool.parameters ? undefined : prefixTextChange(old.parameters, tool.parameters);
|
|
320
|
+
if (description === undefined && parameters === undefined)
|
|
321
|
+
continue;
|
|
322
|
+
out.push({ name: tool.name, change: 'modified', ...description === undefined ? {} : { description }, ...parameters === undefined ? {} : { parameters } });
|
|
323
|
+
}
|
|
324
|
+
for (const tool of previous)
|
|
325
|
+
if (!after.has(tool.name))
|
|
326
|
+
out.push({ name: tool.name, change: 'removed' });
|
|
327
|
+
return out;
|
|
328
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The selected turn's participants as one flat row: the main conversation,
|
|
3
|
+
* then every agent that ran in this turn (external agents and subagents
|
|
4
|
+
* alike) and the turn's compactions. Nothing that did not take part shows.
|
|
5
|
+
* Past four participants beside the main conversation, three stay visible
|
|
6
|
+
* (the selected one always among them) and the rest fold into "+N", which
|
|
7
|
+
* lists them all in the order they started. A note at the row's end holds
|
|
8
|
+
* facts with a jump — the request that delegated the view, a compaction's
|
|
9
|
+
* position — so the tabs keep the header's bottom edge. A failure shows as
|
|
10
|
+
* the tab's ✗ and the failure card below, nowhere else.
|
|
11
|
+
*/
|
|
12
|
+
import { type ReactNode } from 'react';
|
|
13
|
+
import type { AgentScopeOverview, ListTurnsResult, RequestLink } from '../types.ts';
|
|
14
|
+
import type { InspectorTabProps } from './contract.ts';
|
|
15
|
+
type T = InspectorTabProps['t'];
|
|
16
|
+
export interface AgentTabsProps {
|
|
17
|
+
readonly scopes: readonly AgentScopeOverview[];
|
|
18
|
+
readonly value: string;
|
|
19
|
+
readonly onSelect: (scopeId: string) => void;
|
|
20
|
+
readonly onOpenRequest: (link: RequestLink) => void;
|
|
21
|
+
readonly t: T;
|
|
22
|
+
/** The room's decision-model calls this turn: a 房间 tab after the agents when positive. */
|
|
23
|
+
readonly roomCount?: number | undefined;
|
|
24
|
+
}
|
|
25
|
+
/** The 房间 tab's value (no agent scope carries it). */
|
|
26
|
+
export declare const ROOM_TAB = "room:decisions";
|
|
27
|
+
export declare function AgentTabs({ scopes, value, onSelect, onOpenRequest, roomCount, t }: AgentTabsProps): ReactNode;
|
|
28
|
+
/** A session opened on its own that is another session's child. */
|
|
29
|
+
export declare function ParentLine({ parent, t }: {
|
|
30
|
+
readonly parent: NonNullable<ListTurnsResult['parent']>;
|
|
31
|
+
readonly t: T;
|
|
32
|
+
}): ReactNode;
|
|
33
|
+
export {};
|
|
34
|
+
//# sourceMappingURL=AgentTabs.d.ts.map
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The selected turn's participants as one flat row: the main conversation,
|
|
4
|
+
* then every agent that ran in this turn (external agents and subagents
|
|
5
|
+
* alike) and the turn's compactions. Nothing that did not take part shows.
|
|
6
|
+
* Past four participants beside the main conversation, three stay visible
|
|
7
|
+
* (the selected one always among them) and the rest fold into "+N", which
|
|
8
|
+
* lists them all in the order they started. A note at the row's end holds
|
|
9
|
+
* facts with a jump — the request that delegated the view, a compaction's
|
|
10
|
+
* position — so the tabs keep the header's bottom edge. A failure shows as
|
|
11
|
+
* the tab's ✗ and the failure card below, nowhere else.
|
|
12
|
+
*/
|
|
13
|
+
import { useEffect, useRef, useState } from 'react';
|
|
14
|
+
import { requestNumberText } from "./aggregate.js";
|
|
15
|
+
import css from './AgentTabs.module.css';
|
|
16
|
+
/** Participants shown beside the main conversation before the rest fold behind "+N". */
|
|
17
|
+
const VISIBLE = 3;
|
|
18
|
+
function labelOf(scope, t) {
|
|
19
|
+
if (scope.kind === 'main')
|
|
20
|
+
return t('scope.mainName');
|
|
21
|
+
if (scope.kind === 'compaction')
|
|
22
|
+
return t('tabs.compaction');
|
|
23
|
+
return scope.label ?? scope.scopeId;
|
|
24
|
+
}
|
|
25
|
+
function Mark({ scope }) {
|
|
26
|
+
// Success is the norm; only a failure earns a mark.
|
|
27
|
+
if (scope.status === 'failed')
|
|
28
|
+
return _jsx("span", { className: css.bad, "aria-hidden": "true", children: "\u2717" });
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
function Tab({ scope, selected, onSelect, t }) {
|
|
32
|
+
return _jsxs("button", { type: "button", role: "tab", "aria-selected": selected, className: css.tab, onClick: () => onSelect(scope.scopeId), title: labelOf(scope, t), children: [_jsx("span", { className: css.label, children: labelOf(scope, t) }), _jsx(Mark, { scope: scope }), _jsx("span", { className: css.count, children: scope.requests.length })] });
|
|
33
|
+
}
|
|
34
|
+
function Overflow({ hidden, items, value, onSelect, t }) {
|
|
35
|
+
const [open, setOpen] = useState(false);
|
|
36
|
+
const box = useRef(null);
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!open)
|
|
39
|
+
return;
|
|
40
|
+
const onDown = (event) => { if (!box.current?.contains(event.target))
|
|
41
|
+
setOpen(false); };
|
|
42
|
+
const onKey = (event) => { if (event.key === 'Escape')
|
|
43
|
+
setOpen(false); };
|
|
44
|
+
document.addEventListener('pointerdown', onDown);
|
|
45
|
+
document.addEventListener('keydown', onKey);
|
|
46
|
+
return () => { document.removeEventListener('pointerdown', onDown); document.removeEventListener('keydown', onKey); };
|
|
47
|
+
}, [open]);
|
|
48
|
+
return _jsxs("div", { ref: box, className: css.moreBox, children: [_jsxs("button", { type: "button", className: css.more, "aria-expanded": open, "aria-haspopup": "listbox", onClick: () => setOpen(v => !v), children: ["+", hidden, _jsx("svg", { viewBox: "0 0 10 10", "aria-hidden": "true", children: _jsx("path", { d: "M2 3.5 5 6.5 8 3.5", fill: "none", stroke: "currentColor", strokeWidth: "1.4", strokeLinecap: "round", strokeLinejoin: "round" }) })] }), open && _jsxs("div", { className: css.pop, role: "listbox", "aria-label": t('tabs.participants'), children: [_jsxs("div", { className: css.popHead, children: [_jsxs("span", { children: [t('tabs.participants'), " \u00B7 ", items.length] }), _jsx("span", { children: t('tabs.popCols') })] }), items.map(scope => _jsxs("button", { type: "button", role: "option", "aria-selected": scope.scopeId === value, className: css.popRow, title: labelOf(scope, t), onClick: () => { onSelect(scope.scopeId); setOpen(false); }, children: [_jsx("span", { className: css.label, children: labelOf(scope, t) }), _jsx("span", { className: css.mark, children: _jsx(Mark, { scope: scope }) }), _jsx("span", { className: css.num, children: scope.delegatedBy ? `#${requestNumberText(scope.delegatedBy)}` : '—' }), _jsx("span", { className: css.num, children: scope.requests.length })] }, scope.scopeId))] })] });
|
|
49
|
+
}
|
|
50
|
+
/** How the selected view relates to the main conversation: facts with a jump, no explanations. */
|
|
51
|
+
function Relation({ scope, onOpenRequest, t }) {
|
|
52
|
+
const back = (link) => _jsx("button", { type: "button", onClick: () => onOpenRequest(link), children: t('rel.back', { seq: requestNumberText(link) }) });
|
|
53
|
+
const parts = [];
|
|
54
|
+
if (scope.kind === 'compaction') {
|
|
55
|
+
const c = scope.compaction;
|
|
56
|
+
if (c?.replacedCount !== undefined)
|
|
57
|
+
parts.push(_jsx("span", { children: t('rel.compacted', { count: String(c.replacedCount) }) }, "n"));
|
|
58
|
+
if (c?.betweenTurns)
|
|
59
|
+
parts.push(_jsx("span", { children: t('rel.betweenTurns') }, "w"));
|
|
60
|
+
else if (c?.after)
|
|
61
|
+
parts.push(_jsx("span", { children: t('rel.after', { seq: requestNumberText(c.after) }) }, "w"), _jsx("span", { children: back(c.after) }, "b"));
|
|
62
|
+
}
|
|
63
|
+
else if (scope.delegatedBy) {
|
|
64
|
+
parts.push(_jsx("span", { children: t('rel.delegatedBy', { seq: requestNumberText(scope.delegatedBy) }) }, "by"), _jsx("span", { children: back(scope.delegatedBy) }, "b"));
|
|
65
|
+
}
|
|
66
|
+
return parts.length === 0 ? null : _jsx("div", { className: css.relation, children: parts });
|
|
67
|
+
}
|
|
68
|
+
/** The 房间 tab's value (no agent scope carries it). */
|
|
69
|
+
export const ROOM_TAB = 'room:decisions';
|
|
70
|
+
export function AgentTabs({ scopes, value, onSelect, onOpenRequest, roomCount, t }) {
|
|
71
|
+
const selected = scopes.find(scope => scope.scopeId === value) ?? scopes[0];
|
|
72
|
+
const main = scopes.filter(scope => scope.kind === 'main');
|
|
73
|
+
const others = [
|
|
74
|
+
...scopes.filter(scope => scope.kind === 'subagent').sort((a, b) => a.startedAt - b.startedAt),
|
|
75
|
+
...scopes.filter(scope => scope.kind === 'compaction'),
|
|
76
|
+
];
|
|
77
|
+
let shown = others;
|
|
78
|
+
if (others.length > VISIBLE + 1) {
|
|
79
|
+
shown = others.slice(0, VISIBLE);
|
|
80
|
+
const current = others.find(scope => scope.scopeId === value);
|
|
81
|
+
if (current !== undefined && !shown.includes(current))
|
|
82
|
+
shown = [...others.slice(0, VISIBLE - 1), current];
|
|
83
|
+
}
|
|
84
|
+
return _jsx("div", { className: css.root, children: _jsxs("div", { className: css.row, role: "tablist", "aria-label": t('tab.title'), children: [[...main, ...shown].map(scope => _jsx(Tab, { scope: scope, selected: value !== ROOM_TAB && scope.scopeId === selected?.scopeId, onSelect: onSelect, t: t }, scope.scopeId)), shown.length < others.length && _jsx(Overflow, { hidden: others.length - shown.length, items: others, value: value, onSelect: onSelect, t: t }), roomCount !== undefined && roomCount > 0 && (_jsxs("button", { type: "button", role: "tab", "aria-selected": value === ROOM_TAB, className: css.tab, onClick: () => onSelect(ROOM_TAB), children: [_jsx("span", { className: css.label, children: t('room.tab') }), _jsx("span", { className: css.count, children: roomCount })] })), selected && value !== ROOM_TAB && _jsx(Relation, { scope: selected, onOpenRequest: onOpenRequest, t: t })] }) });
|
|
85
|
+
}
|
|
86
|
+
/** A session opened on its own that is another session's child. */
|
|
87
|
+
export function ParentLine({ parent, t }) {
|
|
88
|
+
return _jsxs("div", { className: css.parent, children: [_jsx("span", { className: css.muted, children: t('rel.parent') }), _jsx("b", { children: parent.title ?? parent.sessionId }), parent.hostTurn !== undefined && _jsxs("span", { className: css.muted, children: ["\u00B7 ", t('rel.parentTurn', { turn: String(parent.hostTurn) })] }), parent.label !== undefined && _jsxs("span", { className: css.muted, children: ["\u00B7 ", t('rel.parentLabel', { label: parent.label })] })] });
|
|
89
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cache section's break line and change list. Each card names the host
|
|
3
|
+
* fact behind it (replacement, compaction, prune, or a bare id-level
|
|
4
|
+
* remove/add), shows its producer verbatim, and expands into the exact
|
|
5
|
+
* difference; the card where the prefix first differs carries the break mark
|
|
6
|
+
* and opens by default. Jumps go to the module that owns the detail (the
|
|
7
|
+
* composition filter, the compaction request, the pre-prune request).
|
|
8
|
+
*/
|
|
9
|
+
import { type ReactNode } from 'react';
|
|
10
|
+
import type { CapturedRequest, PrefixTextChange, RequestLink, SegmentCategory } from '../types.ts';
|
|
11
|
+
import type { InspectorTabProps } from './contract.ts';
|
|
12
|
+
type T = InspectorTabProps['t'];
|
|
13
|
+
type Prefix = NonNullable<CapturedRequest['prefix']>;
|
|
14
|
+
export interface ChangeListProps {
|
|
15
|
+
readonly request: CapturedRequest;
|
|
16
|
+
readonly prefix: Prefix;
|
|
17
|
+
readonly onOpenRequest?: ((link: RequestLink) => void) | undefined;
|
|
18
|
+
readonly onShowCategory?: ((category: SegmentCategory) => void) | undefined;
|
|
19
|
+
readonly t: T;
|
|
20
|
+
}
|
|
21
|
+
/** One-line character diff: shared context, then what left and what arrived. */
|
|
22
|
+
export declare function InlineDiff({ text, t }: {
|
|
23
|
+
readonly text: PrefixTextChange;
|
|
24
|
+
readonly t: T;
|
|
25
|
+
}): ReactNode;
|
|
26
|
+
export declare function ChangeList({ request, prefix, onOpenRequest, onShowCategory, t }: ChangeListProps): ReactNode;
|
|
27
|
+
export {};
|
|
28
|
+
//# sourceMappingURL=ChangeList.d.ts.map
|