@zigil/agent-react 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/dist/index.js ADDED
@@ -0,0 +1,1078 @@
1
+ // src/attention.tsx
2
+ import {
3
+ createContext,
4
+ createElement,
5
+ useContext
6
+ } from "react";
7
+ var MAX_SERIALIZED_BYTES = 4 * 1024;
8
+ var MAX_REQUIRED_FIELD_LENGTH = 128;
9
+ var MAX_SELECTIONS = 12;
10
+ var MAX_HISTORY = 40;
11
+ var MAX_FOCUSED_HISTORY = 8;
12
+ var MAX_HOVER_HISTORY = 2;
13
+ var ATTENTION_ACTIVITY_ACTIONS = [
14
+ "focus",
15
+ "select",
16
+ "navigate",
17
+ "edit",
18
+ "execute",
19
+ "approve",
20
+ "dismiss",
21
+ "hover"
22
+ ];
23
+ var AttentionContextReact = createContext(null);
24
+ function AttentionProvider({
25
+ children,
26
+ context
27
+ }) {
28
+ return createElement(
29
+ AttentionContextReact.Provider,
30
+ { value: context },
31
+ children
32
+ );
33
+ }
34
+ function useAttention() {
35
+ return useContext(AttentionContextReact);
36
+ }
37
+ function formatAttentionLabel(attention) {
38
+ const primary = attention?.selection?.label ?? attention?.selection?.id ?? attention?.workspace?.label ?? "this workspace";
39
+ const extraSelections = Math.max(0, (attention?.selections?.length ?? 0) - 1);
40
+ return extraSelections > 0 ? `${primary} +${extraSelections}` : primary;
41
+ }
42
+ var AttentionContextSchema = standardSchema(
43
+ (value) => {
44
+ const issues = validateAttentionContext(value);
45
+ return issues.length > 0 ? issues : value;
46
+ }
47
+ );
48
+ function serializeAttention(context, privacy = "focused") {
49
+ const normalized = normalizeAttention(context, privacy);
50
+ assertClosedAttention(normalized);
51
+ let serialized = JSON.stringify(normalized);
52
+ if (byteLength(serialized) <= MAX_SERIALIZED_BYTES) return serialized;
53
+ warnOversizedAttention();
54
+ while (normalized.history?.some((event) => event.action === "hover")) {
55
+ const hoverIndex = normalized.history.findIndex(
56
+ (event) => event.action === "hover"
57
+ );
58
+ normalized.history.splice(hoverIndex, 1);
59
+ if (normalized.history.length === 0) delete normalized.history;
60
+ serialized = JSON.stringify(normalized);
61
+ if (byteLength(serialized) <= MAX_SERIALIZED_BYTES) return serialized;
62
+ }
63
+ while ((normalized.history?.length ?? 0) > 0) {
64
+ normalized.history?.shift();
65
+ if (normalized.history?.length === 0) delete normalized.history;
66
+ serialized = JSON.stringify(normalized);
67
+ if (byteLength(serialized) <= MAX_SERIALIZED_BYTES) return serialized;
68
+ }
69
+ delete normalized.hover;
70
+ serialized = JSON.stringify(normalized);
71
+ if (byteLength(serialized) <= MAX_SERIALIZED_BYTES) return serialized;
72
+ removeOptionalSelectionFields(normalized);
73
+ serialized = JSON.stringify(normalized);
74
+ if (byteLength(serialized) <= MAX_SERIALIZED_BYTES) return serialized;
75
+ const required = truncateRequiredFields(normalized);
76
+ serialized = JSON.stringify(required);
77
+ while (byteLength(serialized) > MAX_SERIALIZED_BYTES && (required.selections?.length ?? 0) > 0) {
78
+ required.selections?.pop();
79
+ if (required.selections?.length === 0) delete required.selections;
80
+ serialized = JSON.stringify(required);
81
+ }
82
+ return serialized;
83
+ }
84
+ function validateAttentionContext(value, path = []) {
85
+ if (!isRecord(value)) return [issue("Expected attention context", ...path)];
86
+ const issues = [];
87
+ if (!hasOnlyKeys(value, [
88
+ "application",
89
+ "route",
90
+ "workspace",
91
+ "selection",
92
+ "selections",
93
+ "hover",
94
+ "history"
95
+ ]))
96
+ issues.push(issue("Unexpected attention field", ...path));
97
+ if (typeof value.application !== "string")
98
+ issues.push(issue("Expected a string", ...path, "application"));
99
+ if (typeof value.route !== "string")
100
+ issues.push(issue("Expected a string", ...path, "route"));
101
+ if (value.workspace !== void 0)
102
+ issues.push(...validateWorkspace(value.workspace, [...path, "workspace"]));
103
+ if (value.selection !== void 0)
104
+ issues.push(...validateSelection(value.selection, [...path, "selection"]));
105
+ if (value.hover !== void 0)
106
+ issues.push(...validateSelection(value.hover, [...path, "hover"]));
107
+ if (value.selections !== void 0) {
108
+ if (!Array.isArray(value.selections))
109
+ issues.push(issue("Expected selections", ...path, "selections"));
110
+ else
111
+ value.selections.forEach(
112
+ (selection, index) => issues.push(
113
+ ...validateSelection(selection, [...path, "selections", index])
114
+ )
115
+ );
116
+ }
117
+ if (value.history !== void 0) {
118
+ if (!Array.isArray(value.history))
119
+ issues.push(issue("Expected activity history", ...path, "history"));
120
+ else
121
+ value.history.forEach(
122
+ (event, index) => issues.push(...validateActivity(event, [...path, "history", index]))
123
+ );
124
+ }
125
+ return issues;
126
+ }
127
+ function normalizeAttention(context, privacy) {
128
+ const workspace = context.workspace ? {
129
+ kind: context.workspace.kind,
130
+ id: context.workspace.id,
131
+ ...context.workspace.revision === void 0 ? {} : { revision: context.workspace.revision },
132
+ ...privacy === "minimal" || context.workspace.label === void 0 ? {} : { label: context.workspace.label }
133
+ } : void 0;
134
+ const primary = context.selection ? normalizeSelection(context.selection, privacy !== "minimal") : void 0;
135
+ if (privacy === "minimal") {
136
+ return {
137
+ application: context.application,
138
+ route: context.route,
139
+ ...workspace ? { workspace } : {},
140
+ ...primary ? { selection: primary } : {}
141
+ };
142
+ }
143
+ const selections = normalizeSelections(context.selections, primary);
144
+ const history = normalizeHistory(
145
+ context.history,
146
+ privacy === "focused" ? MAX_FOCUSED_HISTORY : MAX_HISTORY,
147
+ privacy === "expanded"
148
+ );
149
+ return {
150
+ application: context.application,
151
+ route: context.route,
152
+ ...workspace ? { workspace } : {},
153
+ ...primary ? { selection: primary } : {},
154
+ ...selections.length > 0 ? { selections } : {},
155
+ ...privacy === "expanded" && context.hover ? { hover: normalizeSelection(context.hover, true) } : {},
156
+ ...history.length > 0 ? { history } : {}
157
+ };
158
+ }
159
+ function normalizeSelections(selections, primary) {
160
+ const ordered = primary ? [
161
+ primary,
162
+ ...(selections ?? []).map((item) => normalizeSelection(item, true))
163
+ ] : (selections ?? []).map((item) => normalizeSelection(item, true));
164
+ const seen = /* @__PURE__ */ new Set();
165
+ return ordered.filter((item) => {
166
+ const key = selectionKey(item);
167
+ if (seen.has(key)) return false;
168
+ seen.add(key);
169
+ return true;
170
+ }).slice(0, MAX_SELECTIONS);
171
+ }
172
+ function normalizeHistory(history, limit, includeHover) {
173
+ const semantic = [];
174
+ const hovers = [];
175
+ for (const event of history ?? []) {
176
+ const normalized = normalizeEvent(event);
177
+ if (normalized.action !== "hover") semantic.push(normalized);
178
+ else if (includeHover) {
179
+ const previous = hovers.at(-1);
180
+ if (previous && selectionKey(previous.target) === selectionKey(normalized.target))
181
+ hovers[hovers.length - 1] = normalized;
182
+ else hovers.push(normalized);
183
+ }
184
+ }
185
+ const retainedSemantic = semantic.slice(-limit);
186
+ const remaining = Math.max(0, limit - retainedSemantic.length);
187
+ const hoverLimit = Math.min(MAX_HOVER_HISTORY, remaining);
188
+ const retainedHover = hoverLimit > 0 ? hovers.slice(-hoverLimit) : [];
189
+ return [...retainedSemantic, ...retainedHover].sort(
190
+ (left, right) => left.timestamp - right.timestamp
191
+ );
192
+ }
193
+ function normalizeEvent(event) {
194
+ return {
195
+ action: event.action,
196
+ target: normalizeSelection(event.target, true),
197
+ timestamp: event.timestamp,
198
+ ...event.summary === void 0 ? {} : { summary: event.summary },
199
+ ...event.detail === void 0 ? {} : { detail: sortRecord(event.detail) }
200
+ };
201
+ }
202
+ function normalizeSelection(selection, includeOptional) {
203
+ return {
204
+ kind: selection.kind,
205
+ id: selection.id,
206
+ ...includeOptional && selection.label !== void 0 ? { label: selection.label } : {},
207
+ ...includeOptional && selection.detail !== void 0 ? { detail: sortRecord(selection.detail) } : {}
208
+ };
209
+ }
210
+ function removeOptionalSelectionFields(context) {
211
+ const selections = [
212
+ context.selection,
213
+ ...context.selections ?? [],
214
+ context.hover
215
+ ].filter((selection) => Boolean(selection));
216
+ for (const selection of selections.reverse()) {
217
+ for (const key of Object.keys(selection.detail ?? {}).reverse()) {
218
+ if (!selection.detail) break;
219
+ delete selection.detail[key];
220
+ if (Object.keys(selection.detail).length === 0) delete selection.detail;
221
+ }
222
+ delete selection.label;
223
+ }
224
+ if (context.workspace) delete context.workspace.label;
225
+ }
226
+ function truncateRequiredFields(context) {
227
+ return {
228
+ application: truncateText(context.application),
229
+ route: truncateText(context.route),
230
+ ...context.workspace ? {
231
+ workspace: {
232
+ kind: truncateText(context.workspace.kind),
233
+ id: truncateText(context.workspace.id),
234
+ ...context.workspace.revision === void 0 ? {} : { revision: context.workspace.revision }
235
+ }
236
+ } : {},
237
+ ...context.selection ? {
238
+ selection: {
239
+ kind: truncateText(context.selection.kind),
240
+ id: truncateText(context.selection.id)
241
+ }
242
+ } : {},
243
+ ...context.selections ? {
244
+ selections: context.selections.slice(0, MAX_SELECTIONS).map((item) => ({
245
+ kind: truncateText(item.kind),
246
+ id: truncateText(item.id)
247
+ }))
248
+ } : {}
249
+ };
250
+ }
251
+ function validateWorkspace(value, path) {
252
+ if (!isRecord(value)) return [issue("Expected a workspace", ...path)];
253
+ const issues = [];
254
+ if (!hasOnlyKeys(value, ["kind", "id", "label", "revision"]))
255
+ issues.push(issue("Unexpected workspace field", ...path));
256
+ for (const key of ["kind", "id"])
257
+ if (typeof value[key] !== "string")
258
+ issues.push(issue("Expected a string", ...path, key));
259
+ if (value.label !== void 0 && typeof value.label !== "string")
260
+ issues.push(issue("Expected a string", ...path, "label"));
261
+ if (value.revision !== void 0 && (typeof value.revision !== "number" || !Number.isFinite(value.revision)))
262
+ issues.push(issue("Expected a finite revision", ...path, "revision"));
263
+ return issues;
264
+ }
265
+ function validateSelection(value, path) {
266
+ if (!isRecord(value)) return [issue("Expected a selection", ...path)];
267
+ const issues = [];
268
+ if (!hasOnlyKeys(value, ["kind", "id", "label", "detail"]))
269
+ issues.push(issue("Unexpected selection field", ...path));
270
+ for (const key of ["kind", "id"])
271
+ if (typeof value[key] !== "string")
272
+ issues.push(issue("Expected a string", ...path, key));
273
+ if (value.label !== void 0 && typeof value.label !== "string")
274
+ issues.push(issue("Expected a string", ...path, "label"));
275
+ issues.push(...validateStringRecord(value.detail, [...path, "detail"]));
276
+ return issues;
277
+ }
278
+ function validateActivity(value, path) {
279
+ if (!isRecord(value)) return [issue("Expected activity", ...path)];
280
+ const issues = [];
281
+ if (!hasOnlyKeys(value, ["action", "target", "timestamp", "summary", "detail"]))
282
+ issues.push(issue("Unexpected activity field", ...path));
283
+ if (typeof value.action !== "string" || !ATTENTION_ACTIVITY_ACTIONS.includes(
284
+ value.action
285
+ ))
286
+ issues.push(issue("Expected an action", ...path, "action"));
287
+ if (typeof value.timestamp !== "number" || !Number.isFinite(value.timestamp))
288
+ issues.push(issue("Expected a finite timestamp", ...path, "timestamp"));
289
+ if (value.summary !== void 0 && typeof value.summary !== "string")
290
+ issues.push(issue("Expected a string", ...path, "summary"));
291
+ issues.push(...validateSelection(value.target, [...path, "target"]));
292
+ issues.push(...validateStringRecord(value.detail, [...path, "detail"]));
293
+ return issues;
294
+ }
295
+ function validateStringRecord(value, path) {
296
+ if (value === void 0) return [];
297
+ if (!isRecord(value) || !Object.values(value).every((item) => typeof item === "string"))
298
+ return [issue("Expected string details", ...path)];
299
+ return [];
300
+ }
301
+ function assertClosedAttention(value) {
302
+ const issues = validateAttentionContext(value);
303
+ if (issues.length > 0)
304
+ throw new Error(`Invalid attention serialization: ${issues[0]?.message}`);
305
+ }
306
+ function standardSchema(validate) {
307
+ return {
308
+ "~standard": {
309
+ version: 1,
310
+ vendor: "sigil-agent",
311
+ validate(value) {
312
+ const result = validate(value);
313
+ return Array.isArray(result) ? { issues: result } : { value: result };
314
+ }
315
+ }
316
+ };
317
+ }
318
+ function issue(message, ...path) {
319
+ return { message, path };
320
+ }
321
+ function isRecord(value) {
322
+ return typeof value === "object" && value !== null && !Array.isArray(value);
323
+ }
324
+ function hasOnlyKeys(value, allowed) {
325
+ const keys = new Set(allowed);
326
+ return Object.keys(value).every((key) => keys.has(key));
327
+ }
328
+ function sortRecord(record) {
329
+ return Object.fromEntries(
330
+ Object.entries(record).sort(([left], [right]) => left.localeCompare(right))
331
+ );
332
+ }
333
+ function selectionKey(selection) {
334
+ return `${selection.kind}\0${selection.id}`;
335
+ }
336
+ function truncateText(value) {
337
+ return value.length <= MAX_REQUIRED_FIELD_LENGTH ? value : `${value.slice(0, MAX_REQUIRED_FIELD_LENGTH - 1)}\u2026`;
338
+ }
339
+ function byteLength(value) {
340
+ return new TextEncoder().encode(value).byteLength;
341
+ }
342
+ function warnOversizedAttention() {
343
+ const isDevelopment = import.meta.env?.DEV === true;
344
+ if (isDevelopment)
345
+ console.warn(
346
+ "Agent attention exceeded 4 KB; optional context was removed before sending."
347
+ );
348
+ }
349
+
350
+ // src/attention-telemetry.ts
351
+ import { useCallback, useEffect, useReducer, useRef } from "react";
352
+ var DEFAULT_HOVER_DELAY_MS = 500;
353
+ var DEFAULT_HISTORY_LIMIT = 40;
354
+ function createAttentionTelemetryState(initialSelections = []) {
355
+ return {
356
+ selections: deduplicateTargets(initialSelections),
357
+ history: []
358
+ };
359
+ }
360
+ function reduceAttentionTelemetry(state, action) {
361
+ switch (action.type) {
362
+ case "select": {
363
+ const exists = state.selections.some(
364
+ (item) => sameAttentionTarget(item, action.target)
365
+ );
366
+ const selections = action.mode === "replace" ? [action.target] : exists ? state.selections.filter(
367
+ (item) => !sameAttentionTarget(item, action.target)
368
+ ) : [action.target, ...state.selections];
369
+ return {
370
+ ...state,
371
+ selections,
372
+ history: appendBoundedActivity(
373
+ state.history,
374
+ action.event,
375
+ action.historyLimit
376
+ )
377
+ };
378
+ }
379
+ case "record":
380
+ return {
381
+ ...state,
382
+ history: appendBoundedActivity(
383
+ state.history,
384
+ action.event,
385
+ action.historyLimit
386
+ )
387
+ };
388
+ case "hover":
389
+ return {
390
+ ...state,
391
+ hover: action.target,
392
+ history: appendBoundedActivity(
393
+ state.history,
394
+ action.event,
395
+ action.historyLimit
396
+ )
397
+ };
398
+ case "end-hover": {
399
+ const { hover: _hover, ...withoutHover } = state;
400
+ return withoutHover;
401
+ }
402
+ case "clear-selections":
403
+ return { ...state, selections: [] };
404
+ case "clear-history":
405
+ return { ...state, history: [] };
406
+ }
407
+ }
408
+ function useAttentionTelemetry({
409
+ initialSelections = [],
410
+ hoverDelayMs = DEFAULT_HOVER_DELAY_MS,
411
+ historyLimit = DEFAULT_HISTORY_LIMIT
412
+ } = {}) {
413
+ const [state, dispatch] = useReducer(
414
+ reduceAttentionTelemetry,
415
+ initialSelections,
416
+ createAttentionTelemetryState
417
+ );
418
+ const hoverTimer = useRef(
419
+ void 0
420
+ );
421
+ const hoverTarget = useRef(void 0);
422
+ useEffect(
423
+ () => () => {
424
+ if (hoverTimer.current) clearTimeout(hoverTimer.current);
425
+ },
426
+ []
427
+ );
428
+ const recordActivity = useCallback(
429
+ (action, target, options = {}) => {
430
+ dispatch({
431
+ type: "record",
432
+ event: createActivity(action, target, options),
433
+ historyLimit
434
+ });
435
+ },
436
+ [historyLimit]
437
+ );
438
+ const select = useCallback(
439
+ (target) => {
440
+ dispatch({
441
+ type: "select",
442
+ target,
443
+ mode: "replace",
444
+ event: createActivity("select", target),
445
+ historyLimit
446
+ });
447
+ },
448
+ [historyLimit]
449
+ );
450
+ const toggleSelection = useCallback(
451
+ (target) => {
452
+ dispatch({
453
+ type: "select",
454
+ target,
455
+ mode: "toggle",
456
+ event: createActivity("select", target),
457
+ historyLimit
458
+ });
459
+ },
460
+ [historyLimit]
461
+ );
462
+ const clearSelections = useCallback(
463
+ () => dispatch({ type: "clear-selections" }),
464
+ []
465
+ );
466
+ const beginHover = useCallback(
467
+ (target) => {
468
+ if (hoverTimer.current) clearTimeout(hoverTimer.current);
469
+ hoverTarget.current = target;
470
+ hoverTimer.current = setTimeout(() => {
471
+ if (!hoverTarget.current || !sameAttentionTarget(hoverTarget.current, target))
472
+ return;
473
+ dispatch({
474
+ type: "hover",
475
+ target,
476
+ event: createActivity("hover", target),
477
+ historyLimit
478
+ });
479
+ }, hoverDelayMs);
480
+ },
481
+ [historyLimit, hoverDelayMs]
482
+ );
483
+ const endHover = useCallback((target) => {
484
+ if (target && hoverTarget.current && !sameAttentionTarget(target, hoverTarget.current))
485
+ return;
486
+ if (hoverTimer.current) clearTimeout(hoverTimer.current);
487
+ hoverTimer.current = void 0;
488
+ hoverTarget.current = void 0;
489
+ dispatch({ type: "end-hover" });
490
+ }, []);
491
+ const clearHistory = useCallback(
492
+ () => dispatch({ type: "clear-history" }),
493
+ []
494
+ );
495
+ return {
496
+ selection: state.selections[0],
497
+ selections: state.selections,
498
+ hover: state.hover,
499
+ history: state.history,
500
+ select,
501
+ toggleSelection,
502
+ clearSelections,
503
+ beginHover,
504
+ endHover,
505
+ recordActivity,
506
+ clearHistory
507
+ };
508
+ }
509
+ function appendBoundedActivity(history, event, limit = DEFAULT_HISTORY_LIMIT) {
510
+ const withoutDuplicateHover = event.action === "hover" ? history.filter(
511
+ (item) => item.action !== "hover" || !sameAttentionTarget(item.target, event.target)
512
+ ) : [...history];
513
+ const next = [...withoutDuplicateHover, event];
514
+ const hoverIndexes = next.map((item, index) => item.action === "hover" ? index : -1).filter((index) => index >= 0);
515
+ for (const index of hoverIndexes.slice(0, -2).reverse())
516
+ next.splice(index, 1);
517
+ while (next.length > Math.max(0, limit)) {
518
+ const firstHover = next.findIndex((item) => item.action === "hover");
519
+ next.splice(firstHover >= 0 ? firstHover : 0, 1);
520
+ }
521
+ return next;
522
+ }
523
+ function sameAttentionTarget(left, right) {
524
+ return left.kind === right.kind && left.id === right.id;
525
+ }
526
+ function createActivity(action, target, {
527
+ timestamp = Date.now(),
528
+ summary,
529
+ detail
530
+ } = {}) {
531
+ return {
532
+ action,
533
+ target,
534
+ timestamp,
535
+ ...summary === void 0 ? {} : { summary },
536
+ ...detail === void 0 ? {} : { detail }
537
+ };
538
+ }
539
+ function deduplicateTargets(selections) {
540
+ const seen = /* @__PURE__ */ new Set();
541
+ return selections.filter((selection) => {
542
+ const key = `${selection.kind}\0${selection.id}`;
543
+ if (seen.has(key)) return false;
544
+ seen.add(key);
545
+ return true;
546
+ });
547
+ }
548
+
549
+ // src/context-draft.ts
550
+ import { useSyncExternalStore } from "react";
551
+ var listeners = /* @__PURE__ */ new Set();
552
+ var drafts = /* @__PURE__ */ new Map();
553
+ var activeDraftScope = "default";
554
+ var MAX_ATTACHMENTS = 6;
555
+ var MAX_ATTACHMENT_TEXT_LENGTH = 192;
556
+ var MAX_CONTEXT_DRAFT_BYTES = 8 * 1024;
557
+ var ContextAttachmentSchema = standardSchema2(
558
+ (value) => {
559
+ const issues = validateAttachment(value);
560
+ return issues.length > 0 ? issues : value;
561
+ }
562
+ );
563
+ var ContextDraftPayloadSchema = standardSchema2(
564
+ (value) => {
565
+ const issues = validateDraftPayload(value);
566
+ return issues.length > 0 ? issues : value;
567
+ }
568
+ );
569
+ function attentionSelectionKey(selection) {
570
+ return `selection:${selection.kind}:${selection.id}`;
571
+ }
572
+ function attentionHistoryKey(event) {
573
+ return `history:${event.timestamp}:${event.action}:${event.target.kind}:${event.target.id}`;
574
+ }
575
+ function getContextDraftScope() {
576
+ return activeDraftScope;
577
+ }
578
+ function getAttentionExclusions() {
579
+ return getDraftState().exclusions;
580
+ }
581
+ function getTurnContextAttachments() {
582
+ return getDraftState().attachments;
583
+ }
584
+ function setContextDraftScope(scopeId) {
585
+ const normalized = scopeId.trim();
586
+ if (!normalized) throw new Error("Context draft scope must be non-empty");
587
+ if (activeDraftScope === normalized) return;
588
+ activeDraftScope = normalized;
589
+ getDraftState();
590
+ emitChange();
591
+ }
592
+ function addContextAttachment(attachment) {
593
+ const validated = validateAttachment(attachment);
594
+ if (validated.length > 0)
595
+ throw new Error(`Invalid context attachment: ${validated[0]?.message}`);
596
+ const normalized = normalizeAttachment(attachment, "expanded");
597
+ const state = getDraftState();
598
+ state.attachments = [
599
+ ...state.attachments.filter((item) => item.id !== normalized.id),
600
+ normalized
601
+ ].slice(-MAX_ATTACHMENTS);
602
+ emitChange();
603
+ }
604
+ function addTurnContextAttachment(selection, retention = "turn") {
605
+ addContextAttachment({
606
+ id: attentionSelectionKey(selection),
607
+ source: "application-selection",
608
+ inclusion: "user-added",
609
+ resource: { kind: selection.kind, id: selection.id },
610
+ label: truncateAttachmentText(selection.label ?? selection.id),
611
+ ...selection.detail ? {
612
+ summary: truncateAttachmentText(
613
+ Object.entries(selection.detail).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${key}: ${value}`).join(" \xB7 ")
614
+ )
615
+ } : {},
616
+ retention
617
+ });
618
+ }
619
+ function removeTurnContextAttachment(id) {
620
+ const state = getDraftState();
621
+ const next = state.attachments.filter((item) => item.id !== id);
622
+ if (next.length === state.attachments.length) return;
623
+ state.attachments = next;
624
+ emitChange();
625
+ }
626
+ function moveTurnContextAttachment(id, direction) {
627
+ const state = getDraftState();
628
+ const index = state.attachments.findIndex((item) => item.id === id);
629
+ const target = index + direction;
630
+ if (index < 0 || target < 0 || target >= state.attachments.length) return;
631
+ const next = [...state.attachments];
632
+ [next[index], next[target]] = [next[target], next[index]];
633
+ state.attachments = next;
634
+ emitChange();
635
+ }
636
+ function setTurnContextAttachmentRetention(id, retention) {
637
+ let changed = false;
638
+ const state = getDraftState();
639
+ state.attachments = state.attachments.map((item) => {
640
+ if (item.id !== id || item.retention === retention) return item;
641
+ changed = true;
642
+ return { ...item, retention };
643
+ });
644
+ if (changed) emitChange();
645
+ }
646
+ function clearTurnContextAttachments() {
647
+ const state = getDraftState();
648
+ const next = state.attachments.filter((item) => item.retention === "session");
649
+ if (next.length === state.attachments.length) return;
650
+ state.attachments = next;
651
+ emitChange();
652
+ }
653
+ function clearContextDraft() {
654
+ const state = getDraftState();
655
+ if (state.exclusions.length === 0 && state.attachments.length === 0) return;
656
+ state.exclusions = [];
657
+ state.attachments = [];
658
+ emitChange();
659
+ }
660
+ function resetContextDraftForTests() {
661
+ drafts.clear();
662
+ activeDraftScope = "default";
663
+ emitChange();
664
+ }
665
+ function setAttentionItemExcluded(key, excluded) {
666
+ const state = getDraftState();
667
+ const next = new Set(state.exclusions);
668
+ if (excluded) next.add(key);
669
+ else next.delete(key);
670
+ state.exclusions = [...next].sort();
671
+ emitChange();
672
+ }
673
+ function clearAttentionExclusions() {
674
+ const state = getDraftState();
675
+ if (state.exclusions.length === 0) return;
676
+ state.exclusions = [];
677
+ emitChange();
678
+ }
679
+ function useAttentionExclusions() {
680
+ return useSyncExternalStore(
681
+ subscribeContextDraft,
682
+ getAttentionExclusions,
683
+ getAttentionExclusions
684
+ );
685
+ }
686
+ function useTurnContextAttachments() {
687
+ return useSyncExternalStore(
688
+ subscribeContextDraft,
689
+ getTurnContextAttachments,
690
+ getTurnContextAttachments
691
+ );
692
+ }
693
+ function serializeAttentionDraft(context, privacy, excludedKeys = getAttentionExclusions(), contextAttachments = getTurnContextAttachments()) {
694
+ const attention = context ? JSON.parse(
695
+ serializeAttention(
696
+ applyAttentionExclusions(context, excludedKeys),
697
+ privacy
698
+ )
699
+ ) : {};
700
+ const normalizedAttachments = contextAttachments.slice(0, MAX_ATTACHMENTS).map((attachment) => normalizeAttachment(attachment, privacy));
701
+ const serialized = fitContextPayload(attention, normalizedAttachments);
702
+ const parsed = JSON.parse(serialized);
703
+ const issues = validateDraftPayload(parsed);
704
+ if (issues.length > 0)
705
+ throw new Error(
706
+ `Invalid context draft serialization: ${issues[0]?.message}`
707
+ );
708
+ return serialized;
709
+ }
710
+ function createAttentionContextPreview(context, privacy, excludedKeys = getAttentionExclusions(), contextAttachments = getTurnContextAttachments()) {
711
+ const serialized = serializeAttentionDraft(
712
+ context,
713
+ privacy,
714
+ excludedKeys,
715
+ contextAttachments
716
+ );
717
+ const normalized = JSON.parse(serialized);
718
+ const selections = normalized.selections?.length ? normalized.selections : normalized.selection ? [normalized.selection] : [];
719
+ const history = normalized.history ?? [];
720
+ const normalizedAttachments = normalized.attachments ?? [];
721
+ const truncatedAttachmentCount = Math.max(
722
+ 0,
723
+ contextAttachments.length - normalizedAttachments.length
724
+ );
725
+ const byteLength3 = new TextEncoder().encode(serialized).byteLength;
726
+ const parts = [
727
+ `${selections.length} selection${selections.length === 1 ? "" : "s"}`,
728
+ `${normalizedAttachments.length} attachment${normalizedAttachments.length === 1 ? "" : "s"}`,
729
+ `${history.length} recent action${history.length === 1 ? "" : "s"}`
730
+ ];
731
+ return {
732
+ attachmentCount: normalizedAttachments.length,
733
+ attachments: normalizedAttachments,
734
+ byteLength: byteLength3,
735
+ estimatedTokens: Math.ceil(byteLength3 / 4),
736
+ formatted: JSON.stringify(normalized, null, 2),
737
+ history,
738
+ selectionCount: selections.length,
739
+ selections,
740
+ serialized,
741
+ summary: parts.join(" \xB7 "),
742
+ truncatedAttachmentCount
743
+ };
744
+ }
745
+ function applyAttentionExclusions(context, excludedKeys) {
746
+ if (excludedKeys.length === 0) return context;
747
+ const excluded = new Set(excludedKeys);
748
+ const selectionCandidates = deduplicateSelections([
749
+ ...context.selection ? [context.selection] : [],
750
+ ...context.selections ?? []
751
+ ]);
752
+ const selections = selectionCandidates.filter(
753
+ (selection) => !excluded.has(attentionSelectionKey(selection))
754
+ );
755
+ const excludedTargets = new Set(
756
+ selectionCandidates.filter((selection) => excluded.has(attentionSelectionKey(selection))).map((selection) => `${selection.kind}\0${selection.id}`)
757
+ );
758
+ const history = (context.history ?? []).filter(
759
+ (event) => !excluded.has(attentionHistoryKey(event)) && !excludedTargets.has(`${event.target.kind}\0${event.target.id}`)
760
+ );
761
+ const hover = context.hover && !excluded.has(attentionSelectionKey(context.hover)) && !excludedTargets.has(`${context.hover.kind}\0${context.hover.id}`) ? context.hover : void 0;
762
+ return {
763
+ ...context,
764
+ selection: selections[0],
765
+ selections: selections.length > 0 ? selections : void 0,
766
+ history: history.length > 0 ? history : void 0,
767
+ hover
768
+ };
769
+ }
770
+ function deduplicateSelections(selections) {
771
+ const seen = /* @__PURE__ */ new Set();
772
+ return selections.filter((selection) => {
773
+ const key = `${selection.kind}\0${selection.id}`;
774
+ if (seen.has(key)) return false;
775
+ seen.add(key);
776
+ return true;
777
+ });
778
+ }
779
+ function subscribeContextDraft(listener) {
780
+ listeners.add(listener);
781
+ return () => listeners.delete(listener);
782
+ }
783
+ function emitChange() {
784
+ listeners.forEach((listener) => listener());
785
+ }
786
+ function getDraftState() {
787
+ let state = drafts.get(activeDraftScope);
788
+ if (!state) {
789
+ state = { attachments: [], exclusions: [] };
790
+ drafts.set(activeDraftScope, state);
791
+ }
792
+ return state;
793
+ }
794
+ function normalizeAttachment(attachment, privacy) {
795
+ return {
796
+ id: truncateAttachmentText(attachment.id),
797
+ source: attachment.source,
798
+ inclusion: attachment.inclusion,
799
+ resource: {
800
+ kind: truncateAttachmentText(attachment.resource.kind),
801
+ id: truncateAttachmentText(attachment.resource.id)
802
+ },
803
+ label: privacy === "minimal" ? truncateAttachmentText(attachment.resource.id) : truncateAttachmentText(attachment.label),
804
+ ...privacy === "minimal" || attachment.summary === void 0 ? {} : { summary: truncateAttachmentText(attachment.summary) },
805
+ retention: attachment.retention
806
+ };
807
+ }
808
+ function fitContextPayload(attention, contextAttachments) {
809
+ const included = contextAttachments.map((attachment) => ({ ...attachment }));
810
+ let serialized = serializeContextPayload(attention, included);
811
+ if (byteLength2(serialized) <= MAX_CONTEXT_DRAFT_BYTES) return serialized;
812
+ for (let index = included.length - 1; index >= 0; index -= 1) {
813
+ if (included[index]?.summary === void 0) continue;
814
+ delete included[index].summary;
815
+ serialized = serializeContextPayload(attention, included);
816
+ if (byteLength2(serialized) <= MAX_CONTEXT_DRAFT_BYTES) return serialized;
817
+ }
818
+ while (included.length > 0) {
819
+ included.pop();
820
+ serialized = serializeContextPayload(attention, included);
821
+ if (byteLength2(serialized) <= MAX_CONTEXT_DRAFT_BYTES) return serialized;
822
+ }
823
+ return serialized;
824
+ }
825
+ function serializeContextPayload(attention, included) {
826
+ return JSON.stringify({
827
+ ...attention,
828
+ ...included.length > 0 ? { attachments: included } : {}
829
+ });
830
+ }
831
+ function validateDraftPayload(value) {
832
+ if (!isRecord2(value)) return [issue2("Expected a context draft payload")];
833
+ const { attachments, ...attention } = value;
834
+ const issues = [];
835
+ if (Object.keys(attention).length > 0)
836
+ issues.push(...validateAttentionContext(attention));
837
+ if (attachments !== void 0) {
838
+ if (!Array.isArray(attachments))
839
+ issues.push(issue2("Expected attachments", "attachments"));
840
+ else
841
+ attachments.forEach(
842
+ (attachment, index) => issues.push(...validateAttachment(attachment, ["attachments", index]))
843
+ );
844
+ }
845
+ return issues;
846
+ }
847
+ function validateAttachment(value, path = []) {
848
+ if (!isRecord2(value)) return [issue2("Expected an attachment", ...path)];
849
+ const issues = [];
850
+ if (!hasOnlyKeys2(value, [
851
+ "id",
852
+ "source",
853
+ "inclusion",
854
+ "resource",
855
+ "label",
856
+ "summary",
857
+ "retention"
858
+ ]))
859
+ issues.push(issue2("Unexpected attachment field", ...path));
860
+ for (const key of ["id", "label"])
861
+ if (typeof value[key] !== "string")
862
+ issues.push(issue2("Expected a string", ...path, key));
863
+ if (value.summary !== void 0 && typeof value.summary !== "string")
864
+ issues.push(issue2("Expected a string", ...path, "summary"));
865
+ if (value.source !== "application-selection" && value.source !== "semantic-fork" && value.source !== "agent-requested")
866
+ issues.push(issue2("Expected a supported source", ...path, "source"));
867
+ if (value.inclusion !== "user-added" && value.inclusion !== "automatic" && value.inclusion !== "agent-requested")
868
+ issues.push(issue2("Expected a supported inclusion", ...path, "inclusion"));
869
+ if (value.retention !== "turn" && value.retention !== "session")
870
+ issues.push(
871
+ issue2("Expected turn or session retention", ...path, "retention")
872
+ );
873
+ if (!isRecord2(value.resource))
874
+ issues.push(issue2("Expected a resource", ...path, "resource"));
875
+ else {
876
+ if (!hasOnlyKeys2(value.resource, ["kind", "id"]))
877
+ issues.push(issue2("Unexpected resource field", ...path, "resource"));
878
+ for (const key of ["kind", "id"])
879
+ if (typeof value.resource[key] !== "string")
880
+ issues.push(issue2("Expected a string", ...path, "resource", key));
881
+ }
882
+ return issues;
883
+ }
884
+ function standardSchema2(validate) {
885
+ return {
886
+ "~standard": {
887
+ version: 1,
888
+ vendor: "sigil-agent",
889
+ validate(value) {
890
+ const result = validate(value);
891
+ return Array.isArray(result) ? { issues: result } : { value: result };
892
+ }
893
+ }
894
+ };
895
+ }
896
+ function issue2(message, ...path) {
897
+ return { message, path };
898
+ }
899
+ function isRecord2(value) {
900
+ return typeof value === "object" && value !== null && !Array.isArray(value);
901
+ }
902
+ function hasOnlyKeys2(value, allowed) {
903
+ const keys = new Set(allowed);
904
+ return Object.keys(value).every((key) => keys.has(key));
905
+ }
906
+ function truncateAttachmentText(value) {
907
+ return value.length <= MAX_ATTACHMENT_TEXT_LENGTH ? value : `${value.slice(0, MAX_ATTACHMENT_TEXT_LENGTH - 1)}\u2026`;
908
+ }
909
+ function byteLength2(value) {
910
+ return new TextEncoder().encode(value).byteLength;
911
+ }
912
+
913
+ // src/context-privacy.ts
914
+ import { useSyncExternalStore as useSyncExternalStore2 } from "react";
915
+ var ATTENTION_PRIVACY_STORAGE_KEY = "sigil-agent:attention-privacy";
916
+ var listeners2 = /* @__PURE__ */ new Set();
917
+ var cachedLevel;
918
+ var listeningForStorage = false;
919
+ var AttentionPrivacyLevelSchema = {
920
+ "~standard": {
921
+ version: 1,
922
+ vendor: "sigil-agent",
923
+ validate(value) {
924
+ return isAttentionPrivacyLevel(value) ? { value } : { issues: [{ message: "Expected minimal, focused, or expanded" }] };
925
+ }
926
+ }
927
+ };
928
+ function getAttentionPrivacyLevel() {
929
+ if (typeof window === "undefined") return "focused";
930
+ if (cachedLevel === void 0)
931
+ cachedLevel = parseAttentionPrivacyLevel(
932
+ window.localStorage.getItem(ATTENTION_PRIVACY_STORAGE_KEY)
933
+ );
934
+ return cachedLevel;
935
+ }
936
+ function setAttentionPrivacyLevel(level) {
937
+ cachedLevel = level;
938
+ if (typeof window !== "undefined")
939
+ window.localStorage.setItem(ATTENTION_PRIVACY_STORAGE_KEY, level);
940
+ emitChange2();
941
+ }
942
+ function useAttentionPrivacyLevel() {
943
+ return useSyncExternalStore2(
944
+ subscribeAttentionPrivacyLevel,
945
+ getAttentionPrivacyLevel,
946
+ () => "focused"
947
+ );
948
+ }
949
+ function subscribeAttentionPrivacyLevel(listener) {
950
+ listeners2.add(listener);
951
+ if (typeof window !== "undefined" && !listeningForStorage) {
952
+ window.addEventListener("storage", handleStorage);
953
+ listeningForStorage = true;
954
+ }
955
+ return () => {
956
+ listeners2.delete(listener);
957
+ if (typeof window !== "undefined" && listeningForStorage && listeners2.size === 0) {
958
+ window.removeEventListener("storage", handleStorage);
959
+ listeningForStorage = false;
960
+ }
961
+ };
962
+ }
963
+ function parseAttentionPrivacyLevel(value) {
964
+ return value === "minimal" || value === "expanded" ? value : "focused";
965
+ }
966
+ function resetAttentionPrivacyForTests() {
967
+ cachedLevel = void 0;
968
+ listeners2.clear();
969
+ if (typeof window !== "undefined" && listeningForStorage)
970
+ window.removeEventListener("storage", handleStorage);
971
+ listeningForStorage = false;
972
+ }
973
+ function handleStorage(event) {
974
+ if (event.key !== ATTENTION_PRIVACY_STORAGE_KEY) return;
975
+ cachedLevel = parseAttentionPrivacyLevel(event.newValue);
976
+ emitChange2();
977
+ }
978
+ function isAttentionPrivacyLevel(value) {
979
+ return value === "minimal" || value === "focused" || value === "expanded";
980
+ }
981
+ function emitChange2() {
982
+ listeners2.forEach((listener) => listener());
983
+ }
984
+
985
+ // src/session.tsx
986
+ import { createContext as createContext2, useContext as useContext2 } from "react";
987
+ import { jsx } from "react/jsx-runtime";
988
+ var AgentRuntimeSessionContext = createContext2(
989
+ null
990
+ );
991
+ function AgentRuntimeSessionProvider({
992
+ children,
993
+ session
994
+ }) {
995
+ return /* @__PURE__ */ jsx(AgentRuntimeSessionContext.Provider, { value: session, children });
996
+ }
997
+ function useAgentRuntimeSession() {
998
+ const session = useContext2(AgentRuntimeSessionContext);
999
+ if (!session) {
1000
+ throw new Error(
1001
+ "useAgentRuntimeSession must be used within <AgentRuntimeSessionProvider>."
1002
+ );
1003
+ }
1004
+ return session;
1005
+ }
1006
+
1007
+ // src/thread-controls.tsx
1008
+ import {
1009
+ createContext as createContext3,
1010
+ createElement as createElement2,
1011
+ useContext as useContext3
1012
+ } from "react";
1013
+ var AgentThreadControlsContext = createContext3(
1014
+ null
1015
+ );
1016
+ function AgentThreadControlsProvider({
1017
+ children,
1018
+ value
1019
+ }) {
1020
+ return createElement2(
1021
+ AgentThreadControlsContext.Provider,
1022
+ { value },
1023
+ children
1024
+ );
1025
+ }
1026
+ function useAgentThreadControls() {
1027
+ return useContext3(AgentThreadControlsContext);
1028
+ }
1029
+ export {
1030
+ ATTENTION_ACTIVITY_ACTIONS,
1031
+ ATTENTION_PRIVACY_STORAGE_KEY,
1032
+ AgentRuntimeSessionProvider,
1033
+ AgentThreadControlsProvider,
1034
+ AttentionContextSchema,
1035
+ AttentionPrivacyLevelSchema,
1036
+ AttentionProvider,
1037
+ ContextAttachmentSchema,
1038
+ ContextDraftPayloadSchema,
1039
+ addContextAttachment,
1040
+ addTurnContextAttachment,
1041
+ appendBoundedActivity,
1042
+ applyAttentionExclusions,
1043
+ attentionHistoryKey,
1044
+ attentionSelectionKey,
1045
+ clearAttentionExclusions,
1046
+ clearContextDraft,
1047
+ clearTurnContextAttachments,
1048
+ createAttentionContextPreview,
1049
+ createAttentionTelemetryState,
1050
+ formatAttentionLabel,
1051
+ getAttentionExclusions,
1052
+ getAttentionPrivacyLevel,
1053
+ getContextDraftScope,
1054
+ getTurnContextAttachments,
1055
+ moveTurnContextAttachment,
1056
+ parseAttentionPrivacyLevel,
1057
+ reduceAttentionTelemetry,
1058
+ removeTurnContextAttachment,
1059
+ resetAttentionPrivacyForTests,
1060
+ resetContextDraftForTests,
1061
+ sameAttentionTarget,
1062
+ serializeAttention,
1063
+ serializeAttentionDraft,
1064
+ setAttentionItemExcluded,
1065
+ setAttentionPrivacyLevel,
1066
+ setContextDraftScope,
1067
+ setTurnContextAttachmentRetention,
1068
+ subscribeAttentionPrivacyLevel,
1069
+ useAgentRuntimeSession,
1070
+ useAgentThreadControls,
1071
+ useAttention,
1072
+ useAttentionExclusions,
1073
+ useAttentionPrivacyLevel,
1074
+ useAttentionTelemetry,
1075
+ useTurnContextAttachments,
1076
+ validateAttentionContext
1077
+ };
1078
+ //# sourceMappingURL=index.js.map