@volter-ai-dev/supercode-ui 0.1.64 → 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 +11 -0
  2. package/host.d.ts +66 -0
  3. package/host.mjs +364 -0
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -237,6 +237,17 @@ The optional attention tracker baselines initial inventory without inventing unr
237
237
  by stable opaque identity, and marks only newer conversation evidence or a proven runtime completion.
238
238
  HTTP, SSE, WebSocket, authentication, and product shell behavior remain host-owned transports.
239
239
 
240
+ A native-store inbox can use `createNativeSessionAttentionTracker` instead. It consumes projected
241
+ session rows plus their native descriptors, persists opaque message cursors, ignores tool and
242
+ heartbeat churn, treats compaction as a new baseline, and returns the delay for one host-owned
243
+ settlement timer. This keeps unread and finished semantics identical in an editor, extension,
244
+ desktop app, or mobile companion without moving file persistence into the UI package.
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
+
240
251
  Native continuation exposes one action and a quiet execution-transport selector. A host with a real
241
252
  terminal provider can add `terminal` to `continuationModes` and handle `onResumeTerminal`; Terminal
242
253
  is then the initial choice and Headless remains available from the selector. The UI never infers
package/host.d.ts CHANGED
@@ -72,6 +72,72 @@ export function createSessionAttentionTracker(
72
72
  options?: SessionAttentionTrackerOptions,
73
73
  ): SessionAttentionTracker;
74
74
 
75
+ export interface NativeSessionAttentionTrackerState {
76
+ version: 1;
77
+ attention: import('./index.js').SessionAttention[];
78
+ observedCursors: Record<string, string>;
79
+ }
80
+
81
+ export interface NativeSessionAttentionObservation {
82
+ sessions: readonly import('./index.js').SessionRowModel[];
83
+ descriptors: readonly import('@volter-ai-dev/supercode-harness-sdk').SessionDescriptor[];
84
+ keyForDescriptor(
85
+ descriptor: import('@volter-ai-dev/supercode-harness-sdk').SessionDescriptor,
86
+ ): string;
87
+ controller?: SupercodeClientSnapshot | null;
88
+ attachedKey?: string | null;
89
+ panelVisible?: boolean;
90
+ now?: number;
91
+ settleMs?: number;
92
+ }
93
+
94
+ export class NativeSessionAttentionTracker {
95
+ constructor(options?: {
96
+ state?: unknown;
97
+ onChange?(state: NativeSessionAttentionTrackerState): void;
98
+ });
99
+ acknowledge(key: string): boolean;
100
+ observe(options: NativeSessionAttentionObservation): {
101
+ attention: import('./index.js').SessionAttention[];
102
+ settleAfterMs: number | null;
103
+ };
104
+ snapshot(): NativeSessionAttentionTrackerState;
105
+ }
106
+
107
+ export function createNativeSessionAttentionTracker(options?: {
108
+ state?: unknown;
109
+ onChange?(state: NativeSessionAttentionTrackerState): void;
110
+ }): NativeSessionAttentionTracker;
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
+
75
141
  export interface RemoteUiBindingOptions
76
142
  extends Pick<
77
143
  UiAdapter,
package/host.mjs CHANGED
@@ -1,12 +1,20 @@
1
1
  import { parseSupercodeUiIntent, sessionActivity } from './core.mjs';
2
2
  import { dispatchControllerIntent, projectClientSnapshot } from './controller.mjs';
3
+ import { conversationPreviewText } from '@volter-ai-dev/supercode-client';
3
4
 
4
5
  const FRAME_SCHEMA = 'supercode.ui-host-state.v1';
6
+ const NATIVE_STATE_LIMIT = 500;
5
7
 
6
8
  function objectRecord(value) {
7
9
  return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
8
10
  }
9
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
+
10
18
  function defaultInstanceId() {
11
19
  if (typeof globalThis.crypto?.randomUUID === 'function') return globalThis.crypto.randomUUID();
12
20
  return `host-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
@@ -321,6 +329,362 @@ export function createSessionAttentionTracker(options) {
321
329
  return new SessionAttentionTracker(options);
322
330
  }
323
331
 
332
+ function nativeAttentionState(value) {
333
+ const parsed = objectRecord(value);
334
+ const attention = Array.isArray(parsed?.attention)
335
+ ? parsed.attention.slice(0, NATIVE_STATE_LIMIT).flatMap((candidate) => {
336
+ const item = objectRecord(candidate);
337
+ if (
338
+ typeof item?.key !== 'string' ||
339
+ !item.key ||
340
+ item.key.length > 200 ||
341
+ !['unseen', 'finished', 'failed'].includes(item.kind)
342
+ ) return [];
343
+ return [{
344
+ key: item.key,
345
+ kind: item.kind,
346
+ unreadCount: Number.isSafeInteger(item.unreadCount)
347
+ ? Math.max(1, Math.min(999, item.unreadCount))
348
+ : 1,
349
+ ...(Number.isSafeInteger(item.afterMessages) && item.afterMessages >= 0
350
+ ? { afterMessages: item.afterMessages }
351
+ : {}),
352
+ ...(typeof item.preview === 'string' && item.preview
353
+ ? { preview: item.preview.slice(0, 240) }
354
+ : {}),
355
+ }];
356
+ })
357
+ : [];
358
+ const cursors = objectRecord(parsed?.observedCursors);
359
+ const observedCursors = cursors
360
+ ? Object.fromEntries(
361
+ Object.entries(cursors).slice(0, NATIVE_STATE_LIMIT).filter(
362
+ ([key, cursor]) => key.length <= 200 && typeof cursor === 'string' && cursor.length <= 200,
363
+ ),
364
+ )
365
+ : {};
366
+ return { version: 1, attention, observedCursors };
367
+ }
368
+
369
+ function conversationObservation(descriptor) {
370
+ const candidates = (descriptor?.latest_message_candidates ?? []).flatMap((candidate) => {
371
+ if (
372
+ typeof candidate?.cursor !== 'string' ||
373
+ !candidate.cursor ||
374
+ (candidate.role !== 'user' && candidate.role !== 'assistant')
375
+ ) return [];
376
+ const preview = conversationPreviewText([candidate]);
377
+ return preview
378
+ ? [{ cursor: candidate.cursor, role: candidate.role, preview }]
379
+ : [];
380
+ });
381
+ return { cursor: candidates[0]?.cursor ?? null, candidates };
382
+ }
383
+
384
+ function assistantDelta(current, priorCursor) {
385
+ if (current.cursor === null || current.cursor === priorCursor) return 0;
386
+ const priorIndex = priorCursor === null
387
+ ? -1
388
+ : current.candidates.findIndex((candidate) => candidate.cursor === priorCursor);
389
+ const newlyObserved = priorIndex >= 0
390
+ ? current.candidates.slice(0, priorIndex)
391
+ : current.candidates;
392
+ return newlyObserved.filter((candidate) => candidate.role === 'assistant').length;
393
+ }
394
+
395
+ /** Native-cursor attention reducer for a messenger-style session catalog. Tool records and
396
+ * heartbeat writes never manufacture unread counts; only new human-visible assistant boundaries
397
+ * do. The host owns persistence and the one settlement timer requested by `observe()`. */
398
+ export class NativeSessionAttentionTracker {
399
+ #attention;
400
+ #observedCursors;
401
+ #observedUpdates = new Map();
402
+ #onChange;
403
+ #priorRuntimeActive = false;
404
+ #priorRuntimeKey = null;
405
+
406
+ constructor(options = {}) {
407
+ const state = nativeAttentionState(options.state);
408
+ this.#attention = new Map(state.attention.map((item) => [item.key, item]));
409
+ this.#observedCursors = new Map(Object.entries(state.observedCursors));
410
+ this.#onChange = options.onChange;
411
+ }
412
+
413
+ observe(options) {
414
+ const rows = Array.isArray(options?.sessions) ? options.sessions : [];
415
+ const descriptors = Array.isArray(options?.descriptors) ? options.descriptors : [];
416
+ if (typeof options?.keyForDescriptor !== 'function') {
417
+ throw new TypeError('NativeSessionAttentionTracker requires keyForDescriptor.');
418
+ }
419
+ const now = Number.isFinite(options.now) ? options.now : Date.now();
420
+ const settleMs = Number.isFinite(options.settleMs) && options.settleMs >= 0
421
+ ? options.settleMs
422
+ : 15_000;
423
+ const descriptorByKey = new Map(
424
+ descriptors.map((descriptor) => [options.keyForDescriptor(descriptor), descriptor]),
425
+ );
426
+ let changed = false;
427
+ let settleAfterMs = null;
428
+
429
+ for (const row of rows) {
430
+ const current = conversationObservation(descriptorByKey.get(row.key));
431
+ let prior = this.#observedUpdates.get(row.key);
432
+ if (prior === undefined) {
433
+ const persistedCursor = this.#observedCursors.get(row.key) ?? null;
434
+ prior = {
435
+ cursor: persistedCursor,
436
+ messages: row.messages,
437
+ runtimeStatus: row.runtimeStatus,
438
+ };
439
+ if (persistedCursor === null) {
440
+ this.#observedUpdates.set(row.key, {
441
+ cursor: current.cursor,
442
+ messages: row.messages,
443
+ runtimeStatus: row.runtimeStatus,
444
+ });
445
+ if (current.cursor !== null) {
446
+ setRecentBounded(this.#observedCursors, row.key, current.cursor);
447
+ changed = true;
448
+ }
449
+ continue;
450
+ }
451
+ }
452
+
453
+ const rewritten = row.messages !== null && prior.messages !== null && row.messages < prior.messages;
454
+ const cursorChanged = current.cursor !== null && current.cursor !== prior.cursor;
455
+ const settledAt = row.previewUpdatedAt ?? row.updatedAt;
456
+ const elapsed = settledAt === null ? null : now - settledAt;
457
+ const settled = elapsed !== null && elapsed >= settleMs;
458
+ const completed = prior.runtimeStatus === 'busy' && row.runtimeStatus === 'idle';
459
+
460
+ if (rewritten) {
461
+ prior.cursor = current.cursor;
462
+ } else if (cursorChanged && row.runtimeStatus !== 'busy' && (completed || settled)) {
463
+ const delta = assistantDelta(current, prior.cursor);
464
+ if (delta > 0) {
465
+ changed = this.#mark(
466
+ row.key,
467
+ completed ? 'finished' : 'unseen',
468
+ row.preview,
469
+ delta,
470
+ prior.messages,
471
+ ) || changed;
472
+ }
473
+ prior.cursor = current.cursor;
474
+ } else if (cursorChanged && row.runtimeStatus !== 'busy' && elapsed !== null) {
475
+ const remaining = Math.max(0, settleMs - elapsed);
476
+ settleAfterMs = settleAfterMs === null ? remaining : Math.min(settleAfterMs, remaining);
477
+ }
478
+
479
+ if (prior.cursor !== null && this.#observedCursors.get(row.key) !== prior.cursor) {
480
+ setRecentBounded(this.#observedCursors, row.key, prior.cursor);
481
+ changed = true;
482
+ }
483
+ this.#observedUpdates.set(row.key, {
484
+ cursor: prior.cursor,
485
+ messages: row.messages,
486
+ runtimeStatus: row.runtimeStatus,
487
+ });
488
+ }
489
+
490
+ const controller = options.controller;
491
+ const runtimeActive = controller
492
+ ? controller.turn?.state !== 'idle' || controller.requests?.some((request) => request.status === 'pending')
493
+ : false;
494
+ const runtimeKey = options.attachedKey || this.#priorRuntimeKey;
495
+ if (
496
+ this.#priorRuntimeActive &&
497
+ !runtimeActive &&
498
+ this.#priorRuntimeKey &&
499
+ !(options.panelVisible === true && options.attachedKey === this.#priorRuntimeKey)
500
+ ) {
501
+ const lastAssistant = [...(controller?.conversation ?? [])].reverse().find(
502
+ (entry) => entry.kind === 'message' && entry.role === 'assistant' && entry.text.trim(),
503
+ );
504
+ const preview = lastAssistant?.kind === 'message'
505
+ ? lastAssistant.text.replace(/\s+/g, ' ').trim()
506
+ : undefined;
507
+ changed = this.#mark(
508
+ this.#priorRuntimeKey,
509
+ controller?.error ? 'failed' : 'finished',
510
+ preview,
511
+ ) || changed;
512
+ }
513
+ this.#priorRuntimeActive = runtimeActive;
514
+ this.#priorRuntimeKey = runtimeActive ? runtimeKey : null;
515
+
516
+ if (changed) this.#emit();
517
+ const visible = new Set(rows.map((row) => row.key));
518
+ return {
519
+ attention: [...this.#attention.values()].filter((item) => visible.has(item.key)),
520
+ settleAfterMs,
521
+ };
522
+ }
523
+
524
+ acknowledge(key) {
525
+ if (!this.#attention.delete(key)) return false;
526
+ this.#emit();
527
+ return true;
528
+ }
529
+
530
+ snapshot() {
531
+ return {
532
+ version: 1,
533
+ attention: structuredClone([...this.#attention.values()]),
534
+ observedCursors: Object.fromEntries(this.#observedCursors),
535
+ };
536
+ }
537
+
538
+ #mark(key, kind, preview, unreadDelta = 1, afterMessages = null) {
539
+ const prior = this.#attention.get(key);
540
+ const boundedPreview = typeof preview === 'string' && preview
541
+ ? preview.slice(0, 240)
542
+ : undefined;
543
+ const sameEvent = boundedPreview
544
+ ? prior?.preview === boundedPreview
545
+ : prior?.kind === kind && prior.preview === undefined;
546
+ const unreadCount = sameEvent
547
+ ? (prior?.unreadCount ?? Math.max(1, unreadDelta))
548
+ : Math.min((prior?.unreadCount ?? 0) + Math.max(1, unreadDelta), 999);
549
+ const next = {
550
+ key,
551
+ kind: prior?.kind === 'failed' && kind !== 'failed' ? 'failed' : kind,
552
+ unreadCount,
553
+ ...(prior?.afterMessages !== undefined
554
+ ? { afterMessages: prior.afterMessages }
555
+ : Number.isSafeInteger(afterMessages) && afterMessages >= 0
556
+ ? { afterMessages }
557
+ : {}),
558
+ ...(boundedPreview ? { preview: boundedPreview } : {}),
559
+ };
560
+ if (JSON.stringify(prior) === JSON.stringify(next)) return false;
561
+ setRecentBounded(this.#attention, key, next);
562
+ return true;
563
+ }
564
+
565
+ #emit() {
566
+ this.#onChange?.(this.snapshot());
567
+ }
568
+ }
569
+
570
+ export function createNativeSessionAttentionTracker(options) {
571
+ return new NativeSessionAttentionTracker(options);
572
+ }
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
+
324
688
  /** Browser binding for any transport that can post one intent and return an
325
689
  * optional authoritative frame. Local host actions may intercept an intent. */
326
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.64",
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": {