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

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 +6 -0
  2. package/host.d.ts +37 -0
  3. package/host.mjs +241 -0
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -237,6 +237,12 @@ 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
+
240
246
  Native continuation exposes one action and a quiet execution-transport selector. A host with a real
241
247
  terminal provider can add `terminal` to `continuationModes` and handle `onResumeTerminal`; Terminal
242
248
  is then the initial choice and Headless remains available from the selector. The UI never infers
package/host.d.ts CHANGED
@@ -72,6 +72,43 @@ 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
+
75
112
  export interface RemoteUiBindingOptions
76
113
  extends Pick<
77
114
  UiAdapter,
package/host.mjs CHANGED
@@ -1,5 +1,6 @@
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';
5
6
 
@@ -321,6 +322,246 @@ export function createSessionAttentionTracker(options) {
321
322
  return new SessionAttentionTracker(options);
322
323
  }
323
324
 
325
+ function nativeAttentionState(value) {
326
+ const parsed = objectRecord(value);
327
+ const attention = Array.isArray(parsed?.attention)
328
+ ? parsed.attention.slice(0, 500).flatMap((candidate) => {
329
+ const item = objectRecord(candidate);
330
+ if (
331
+ typeof item?.key !== 'string' ||
332
+ !['unseen', 'finished', 'failed'].includes(item.kind)
333
+ ) return [];
334
+ return [{
335
+ key: item.key,
336
+ kind: item.kind,
337
+ unreadCount: Number.isSafeInteger(item.unreadCount)
338
+ ? Math.max(1, Math.min(999, item.unreadCount))
339
+ : 1,
340
+ ...(Number.isSafeInteger(item.afterMessages) && item.afterMessages >= 0
341
+ ? { afterMessages: item.afterMessages }
342
+ : {}),
343
+ ...(typeof item.preview === 'string' && item.preview
344
+ ? { preview: item.preview.slice(0, 240) }
345
+ : {}),
346
+ }];
347
+ })
348
+ : [];
349
+ const cursors = objectRecord(parsed?.observedCursors);
350
+ const observedCursors = cursors
351
+ ? Object.fromEntries(
352
+ Object.entries(cursors).slice(0, 500).filter(
353
+ ([key, cursor]) => key.length <= 200 && typeof cursor === 'string' && cursor.length <= 200,
354
+ ),
355
+ )
356
+ : {};
357
+ return { version: 1, attention, observedCursors };
358
+ }
359
+
360
+ function conversationObservation(descriptor) {
361
+ const candidates = (descriptor?.latest_message_candidates ?? []).flatMap((candidate) => {
362
+ if (
363
+ typeof candidate?.cursor !== 'string' ||
364
+ !candidate.cursor ||
365
+ (candidate.role !== 'user' && candidate.role !== 'assistant')
366
+ ) return [];
367
+ const preview = conversationPreviewText([candidate]);
368
+ return preview
369
+ ? [{ cursor: candidate.cursor, role: candidate.role, preview }]
370
+ : [];
371
+ });
372
+ return { cursor: candidates[0]?.cursor ?? null, candidates };
373
+ }
374
+
375
+ function assistantDelta(current, priorCursor) {
376
+ if (current.cursor === null || current.cursor === priorCursor) return 0;
377
+ const priorIndex = priorCursor === null
378
+ ? -1
379
+ : current.candidates.findIndex((candidate) => candidate.cursor === priorCursor);
380
+ const newlyObserved = priorIndex >= 0
381
+ ? current.candidates.slice(0, priorIndex)
382
+ : current.candidates;
383
+ return newlyObserved.filter((candidate) => candidate.role === 'assistant').length;
384
+ }
385
+
386
+ /** Native-cursor attention reducer for a messenger-style session catalog. Tool records and
387
+ * heartbeat writes never manufacture unread counts; only new human-visible assistant boundaries
388
+ * do. The host owns persistence and the one settlement timer requested by `observe()`. */
389
+ export class NativeSessionAttentionTracker {
390
+ #attention;
391
+ #observedCursors;
392
+ #observedUpdates = new Map();
393
+ #onChange;
394
+ #priorRuntimeActive = false;
395
+ #priorRuntimeKey = null;
396
+
397
+ constructor(options = {}) {
398
+ const state = nativeAttentionState(options.state);
399
+ this.#attention = new Map(state.attention.map((item) => [item.key, item]));
400
+ this.#observedCursors = new Map(Object.entries(state.observedCursors));
401
+ this.#onChange = options.onChange;
402
+ }
403
+
404
+ observe(options) {
405
+ const rows = Array.isArray(options?.sessions) ? options.sessions : [];
406
+ const descriptors = Array.isArray(options?.descriptors) ? options.descriptors : [];
407
+ if (typeof options?.keyForDescriptor !== 'function') {
408
+ throw new TypeError('NativeSessionAttentionTracker requires keyForDescriptor.');
409
+ }
410
+ const now = Number.isFinite(options.now) ? options.now : Date.now();
411
+ const settleMs = Number.isFinite(options.settleMs) && options.settleMs >= 0
412
+ ? options.settleMs
413
+ : 15_000;
414
+ const descriptorByKey = new Map(
415
+ descriptors.map((descriptor) => [options.keyForDescriptor(descriptor), descriptor]),
416
+ );
417
+ let changed = false;
418
+ let settleAfterMs = null;
419
+
420
+ for (const row of rows) {
421
+ const current = conversationObservation(descriptorByKey.get(row.key));
422
+ let prior = this.#observedUpdates.get(row.key);
423
+ if (prior === undefined) {
424
+ const persistedCursor = this.#observedCursors.get(row.key) ?? null;
425
+ prior = {
426
+ cursor: persistedCursor,
427
+ messages: row.messages,
428
+ runtimeStatus: row.runtimeStatus,
429
+ };
430
+ if (persistedCursor === null) {
431
+ this.#observedUpdates.set(row.key, {
432
+ cursor: current.cursor,
433
+ messages: row.messages,
434
+ runtimeStatus: row.runtimeStatus,
435
+ });
436
+ if (current.cursor !== null) {
437
+ this.#observedCursors.set(row.key, current.cursor);
438
+ changed = true;
439
+ }
440
+ continue;
441
+ }
442
+ }
443
+
444
+ const rewritten = row.messages !== null && prior.messages !== null && row.messages < prior.messages;
445
+ const cursorChanged = current.cursor !== null && current.cursor !== prior.cursor;
446
+ const settledAt = row.previewUpdatedAt ?? row.updatedAt;
447
+ const elapsed = settledAt === null ? null : now - settledAt;
448
+ const settled = elapsed !== null && elapsed >= settleMs;
449
+ const completed = prior.runtimeStatus === 'busy' && row.runtimeStatus === 'idle';
450
+
451
+ if (rewritten) {
452
+ prior.cursor = current.cursor;
453
+ } else if (cursorChanged && row.runtimeStatus !== 'busy' && (completed || settled)) {
454
+ const delta = assistantDelta(current, prior.cursor);
455
+ if (delta > 0) {
456
+ changed = this.#mark(
457
+ row.key,
458
+ completed ? 'finished' : 'unseen',
459
+ row.preview,
460
+ delta,
461
+ prior.messages,
462
+ ) || changed;
463
+ }
464
+ prior.cursor = current.cursor;
465
+ } else if (cursorChanged && row.runtimeStatus !== 'busy' && elapsed !== null) {
466
+ const remaining = Math.max(0, settleMs - elapsed);
467
+ settleAfterMs = settleAfterMs === null ? remaining : Math.min(settleAfterMs, remaining);
468
+ }
469
+
470
+ if (prior.cursor !== null && this.#observedCursors.get(row.key) !== prior.cursor) {
471
+ this.#observedCursors.set(row.key, prior.cursor);
472
+ changed = true;
473
+ }
474
+ this.#observedUpdates.set(row.key, {
475
+ cursor: prior.cursor,
476
+ messages: row.messages,
477
+ runtimeStatus: row.runtimeStatus,
478
+ });
479
+ }
480
+
481
+ const controller = options.controller;
482
+ const runtimeActive = controller
483
+ ? controller.turn?.state !== 'idle' || controller.requests?.some((request) => request.status === 'pending')
484
+ : false;
485
+ const runtimeKey = options.attachedKey || this.#priorRuntimeKey;
486
+ if (
487
+ this.#priorRuntimeActive &&
488
+ !runtimeActive &&
489
+ this.#priorRuntimeKey &&
490
+ !(options.panelVisible === true && options.attachedKey === this.#priorRuntimeKey)
491
+ ) {
492
+ const lastAssistant = [...(controller?.conversation ?? [])].reverse().find(
493
+ (entry) => entry.kind === 'message' && entry.role === 'assistant' && entry.text.trim(),
494
+ );
495
+ const preview = lastAssistant?.kind === 'message'
496
+ ? lastAssistant.text.replace(/\s+/g, ' ').trim()
497
+ : undefined;
498
+ changed = this.#mark(
499
+ this.#priorRuntimeKey,
500
+ controller?.error ? 'failed' : 'finished',
501
+ preview,
502
+ ) || changed;
503
+ }
504
+ this.#priorRuntimeActive = runtimeActive;
505
+ this.#priorRuntimeKey = runtimeActive ? runtimeKey : null;
506
+
507
+ if (changed) this.#emit();
508
+ const visible = new Set(rows.map((row) => row.key));
509
+ return {
510
+ attention: [...this.#attention.values()].filter((item) => visible.has(item.key)),
511
+ settleAfterMs,
512
+ };
513
+ }
514
+
515
+ acknowledge(key) {
516
+ if (!this.#attention.delete(key)) return false;
517
+ this.#emit();
518
+ return true;
519
+ }
520
+
521
+ snapshot() {
522
+ return {
523
+ version: 1,
524
+ attention: structuredClone([...this.#attention.values()]),
525
+ observedCursors: Object.fromEntries(this.#observedCursors),
526
+ };
527
+ }
528
+
529
+ #mark(key, kind, preview, unreadDelta = 1, afterMessages = null) {
530
+ const prior = this.#attention.get(key);
531
+ const boundedPreview = typeof preview === 'string' && preview
532
+ ? preview.slice(0, 240)
533
+ : undefined;
534
+ const sameEvent = boundedPreview
535
+ ? prior?.preview === boundedPreview
536
+ : prior?.kind === kind && prior.preview === undefined;
537
+ const unreadCount = sameEvent
538
+ ? (prior?.unreadCount ?? Math.max(1, unreadDelta))
539
+ : Math.min((prior?.unreadCount ?? 0) + Math.max(1, unreadDelta), 999);
540
+ const next = {
541
+ key,
542
+ kind: prior?.kind === 'failed' && kind !== 'failed' ? 'failed' : kind,
543
+ unreadCount,
544
+ ...(prior?.afterMessages !== undefined
545
+ ? { afterMessages: prior.afterMessages }
546
+ : Number.isSafeInteger(afterMessages) && afterMessages >= 0
547
+ ? { afterMessages }
548
+ : {}),
549
+ ...(boundedPreview ? { preview: boundedPreview } : {}),
550
+ };
551
+ if (JSON.stringify(prior) === JSON.stringify(next)) return false;
552
+ this.#attention.set(key, next);
553
+ return true;
554
+ }
555
+
556
+ #emit() {
557
+ this.#onChange?.(this.snapshot());
558
+ }
559
+ }
560
+
561
+ export function createNativeSessionAttentionTracker(options) {
562
+ return new NativeSessionAttentionTracker(options);
563
+ }
564
+
324
565
  /** Browser binding for any transport that can post one intent and return an
325
566
  * optional authoritative frame. Local host actions may intercept an intent. */
326
567
  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.65",
4
4
  "type": "module",
5
5
  "description": "Composable default UI kit for Supercode-powered coding-agent experiences",
6
6
  "exports": {