@volter-ai-dev/supercode-ui 0.1.65 → 0.1.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +5 -0
  2. package/host.d.ts +29 -0
  3. package/host.mjs +128 -5
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -243,6 +243,11 @@ heartbeat churn, treats compaction as a new baseline, and returns the delay for
243
243
  settlement timer. This keeps unread and finished semantics identical in an editor, extension,
244
244
  desktop app, or mobile companion without moving file persistence into the UI package.
245
245
 
246
+ `createNativeMessengerState` wraps that ledger with bounded per-conversation drafts and per-harness
247
+ Terminal/Headless preferences. It emits one serializable snapshot, so native hosts do not need to
248
+ duplicate state parsing or coordinate several independent maps. The host still owns where that
249
+ snapshot is stored, its debounce and encryption policy, and any multi-device synchronization.
250
+
246
251
  Native continuation exposes one action and a quiet execution-transport selector. A host with a real
247
252
  terminal provider can add `terminal` to `continuationModes` and handle `onResumeTerminal`; Terminal
248
253
  is then the initial choice and Headless remains available from the selector. The UI never infers
package/host.d.ts CHANGED
@@ -109,6 +109,35 @@ export function createNativeSessionAttentionTracker(options?: {
109
109
  onChange?(state: NativeSessionAttentionTrackerState): void;
110
110
  }): NativeSessionAttentionTracker;
111
111
 
112
+ export interface NativeMessengerStateSnapshot extends NativeSessionAttentionTrackerState {
113
+ drafts: Record<string, string>;
114
+ preferredLaunchModes: Record<string, 'headless' | 'terminal'>;
115
+ }
116
+
117
+ export function normalizeNativeMessengerState(value: unknown): NativeMessengerStateSnapshot;
118
+
119
+ export class NativeMessengerState {
120
+ constructor(options?: {
121
+ state?: unknown;
122
+ onChange?(state: NativeMessengerStateSnapshot): void;
123
+ });
124
+ acknowledge(key: string): boolean;
125
+ observeAttention(options: NativeSessionAttentionObservation): {
126
+ attention: import('./index.js').SessionAttention[];
127
+ settleAfterMs: number | null;
128
+ };
129
+ draft(key: string): string;
130
+ setDraft(key: string, draft: string): boolean;
131
+ preferredLaunchMode(harness: string): 'headless' | 'terminal' | null;
132
+ setPreferredLaunchMode(harness: string, mode: 'headless' | 'terminal'): boolean;
133
+ snapshot(): NativeMessengerStateSnapshot;
134
+ }
135
+
136
+ export function createNativeMessengerState(options?: {
137
+ state?: unknown;
138
+ onChange?(state: NativeMessengerStateSnapshot): void;
139
+ }): NativeMessengerState;
140
+
112
141
  export interface RemoteUiBindingOptions
113
142
  extends Pick<
114
143
  UiAdapter,
package/host.mjs CHANGED
@@ -3,11 +3,18 @@ import { dispatchControllerIntent, projectClientSnapshot } from './controller.mj
3
3
  import { conversationPreviewText } from '@volter-ai-dev/supercode-client';
4
4
 
5
5
  const FRAME_SCHEMA = 'supercode.ui-host-state.v1';
6
+ const NATIVE_STATE_LIMIT = 500;
6
7
 
7
8
  function objectRecord(value) {
8
9
  return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
9
10
  }
10
11
 
12
+ function setRecentBounded(map, key, value) {
13
+ map.delete(key);
14
+ map.set(key, value);
15
+ while (map.size > NATIVE_STATE_LIMIT) map.delete(map.keys().next().value);
16
+ }
17
+
11
18
  function defaultInstanceId() {
12
19
  if (typeof globalThis.crypto?.randomUUID === 'function') return globalThis.crypto.randomUUID();
13
20
  return `host-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
@@ -325,10 +332,12 @@ export function createSessionAttentionTracker(options) {
325
332
  function nativeAttentionState(value) {
326
333
  const parsed = objectRecord(value);
327
334
  const attention = Array.isArray(parsed?.attention)
328
- ? parsed.attention.slice(0, 500).flatMap((candidate) => {
335
+ ? parsed.attention.slice(0, NATIVE_STATE_LIMIT).flatMap((candidate) => {
329
336
  const item = objectRecord(candidate);
330
337
  if (
331
338
  typeof item?.key !== 'string' ||
339
+ !item.key ||
340
+ item.key.length > 200 ||
332
341
  !['unseen', 'finished', 'failed'].includes(item.kind)
333
342
  ) return [];
334
343
  return [{
@@ -349,7 +358,7 @@ function nativeAttentionState(value) {
349
358
  const cursors = objectRecord(parsed?.observedCursors);
350
359
  const observedCursors = cursors
351
360
  ? Object.fromEntries(
352
- Object.entries(cursors).slice(0, 500).filter(
361
+ Object.entries(cursors).slice(0, NATIVE_STATE_LIMIT).filter(
353
362
  ([key, cursor]) => key.length <= 200 && typeof cursor === 'string' && cursor.length <= 200,
354
363
  ),
355
364
  )
@@ -434,7 +443,7 @@ export class NativeSessionAttentionTracker {
434
443
  runtimeStatus: row.runtimeStatus,
435
444
  });
436
445
  if (current.cursor !== null) {
437
- this.#observedCursors.set(row.key, current.cursor);
446
+ setRecentBounded(this.#observedCursors, row.key, current.cursor);
438
447
  changed = true;
439
448
  }
440
449
  continue;
@@ -468,7 +477,7 @@ export class NativeSessionAttentionTracker {
468
477
  }
469
478
 
470
479
  if (prior.cursor !== null && this.#observedCursors.get(row.key) !== prior.cursor) {
471
- this.#observedCursors.set(row.key, prior.cursor);
480
+ setRecentBounded(this.#observedCursors, row.key, prior.cursor);
472
481
  changed = true;
473
482
  }
474
483
  this.#observedUpdates.set(row.key, {
@@ -549,7 +558,7 @@ export class NativeSessionAttentionTracker {
549
558
  ...(boundedPreview ? { preview: boundedPreview } : {}),
550
559
  };
551
560
  if (JSON.stringify(prior) === JSON.stringify(next)) return false;
552
- this.#attention.set(key, next);
561
+ setRecentBounded(this.#attention, key, next);
553
562
  return true;
554
563
  }
555
564
 
@@ -562,6 +571,120 @@ export function createNativeSessionAttentionTracker(options) {
562
571
  return new NativeSessionAttentionTracker(options);
563
572
  }
564
573
 
574
+ export function normalizeNativeMessengerState(value) {
575
+ const parsed = objectRecord(value);
576
+ const attention = nativeAttentionState(parsed);
577
+ const draftRecord = objectRecord(parsed?.drafts);
578
+ const drafts = draftRecord
579
+ ? Object.fromEntries(
580
+ Object.entries(draftRecord).slice(0, NATIVE_STATE_LIMIT).flatMap(([key, draft]) =>
581
+ key && key.length <= 200 && typeof draft === 'string' && draft
582
+ ? [[key, draft.slice(0, 50_000)]]
583
+ : [],
584
+ ),
585
+ )
586
+ : {};
587
+ const modeRecord = objectRecord(parsed?.preferredLaunchModes);
588
+ const preferredLaunchModes = modeRecord
589
+ ? Object.fromEntries(
590
+ Object.entries(modeRecord).slice(0, NATIVE_STATE_LIMIT).filter(
591
+ ([key, mode]) =>
592
+ key && key.length <= 200 && (mode === 'headless' || mode === 'terminal'),
593
+ ),
594
+ )
595
+ : {};
596
+ return {
597
+ version: 1,
598
+ attention: attention.attention,
599
+ observedCursors: attention.observedCursors,
600
+ drafts,
601
+ preferredLaunchModes,
602
+ };
603
+ }
604
+
605
+ /** One serializable, bounded state owner for native-session messenger chrome. Storage location,
606
+ * encryption, debounce, and multi-device synchronization remain host policy. */
607
+ export class NativeMessengerState {
608
+ #attention;
609
+ #drafts;
610
+ #onChange;
611
+ #preferredLaunchModes;
612
+
613
+ constructor(options = {}) {
614
+ const state = normalizeNativeMessengerState(options.state);
615
+ this.#onChange = options.onChange;
616
+ this.#drafts = new Map(Object.entries(state.drafts));
617
+ this.#preferredLaunchModes = new Map(Object.entries(state.preferredLaunchModes));
618
+ this.#attention = new NativeSessionAttentionTracker({
619
+ state,
620
+ onChange: () => this.#emit(),
621
+ });
622
+ }
623
+
624
+ observeAttention(options) {
625
+ return this.#attention.observe(options);
626
+ }
627
+
628
+ acknowledge(key) {
629
+ return this.#attention.acknowledge(key);
630
+ }
631
+
632
+ draft(key) {
633
+ return this.#drafts.get(key) ?? '';
634
+ }
635
+
636
+ setDraft(key, draft) {
637
+ if (typeof key !== 'string' || !key || key.length > 200 || typeof draft !== 'string') {
638
+ throw new TypeError('NativeMessengerState draft needs a bounded key and string value.');
639
+ }
640
+ const next = draft.slice(0, 50_000);
641
+ const previous = this.#drafts.get(key) ?? '';
642
+ if (previous === next) return false;
643
+ if (next) setRecentBounded(this.#drafts, key, next);
644
+ else this.#drafts.delete(key);
645
+ this.#emit();
646
+ return true;
647
+ }
648
+
649
+ preferredLaunchMode(harness) {
650
+ return this.#preferredLaunchModes.get(harness) ?? null;
651
+ }
652
+
653
+ setPreferredLaunchMode(harness, mode) {
654
+ if (
655
+ typeof harness !== 'string' ||
656
+ !harness ||
657
+ harness.length > 200 ||
658
+ (mode !== 'headless' && mode !== 'terminal')
659
+ ) {
660
+ throw new TypeError('NativeMessengerState launch preference must be headless or terminal.');
661
+ }
662
+ if (this.#preferredLaunchModes.get(harness) === mode) return false;
663
+ setRecentBounded(this.#preferredLaunchModes, harness, mode);
664
+ this.#emit();
665
+ return true;
666
+ }
667
+
668
+ snapshot() {
669
+ const attention = this.#attention.snapshot();
670
+ return {
671
+ version: 1,
672
+ attention: attention.attention,
673
+ observedCursors: attention.observedCursors,
674
+ drafts: Object.fromEntries(this.#drafts),
675
+ preferredLaunchModes: Object.fromEntries(this.#preferredLaunchModes),
676
+ };
677
+ }
678
+
679
+ #emit() {
680
+ this.#onChange?.(this.snapshot());
681
+ }
682
+ }
683
+
684
+ export function createNativeMessengerState(options) {
685
+ return new NativeMessengerState(options);
686
+ }
687
+
565
688
  /** Browser binding for any transport that can post one intent and return an
566
689
  * optional authoritative frame. Local host actions may intercept an intent. */
567
690
  export function createRemoteUiBinding(options) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter-ai-dev/supercode-ui",
3
- "version": "0.1.65",
3
+ "version": "0.1.66",
4
4
  "type": "module",
5
5
  "description": "Composable default UI kit for Supercode-powered coding-agent experiences",
6
6
  "exports": {