@volter-ai-dev/supercode-ui 0.1.63 → 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.
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
@@ -263,6 +269,9 @@ opaque-key callback and retains the reversible locator map. Its `isWritable` cap
263
269
  is fail-closed: unread badges remain neutral until the host proves a real send or terminal-control
264
270
  path. Runtime activity remains independent, so a read-only channel can still truthfully show that
265
271
  its external agent is working or needs input.
272
+ `matchesSessionRef`, `projectAttachedSession`, and `formatWorkspacePath` complete that trusted-host
273
+ adapter so products do not need local copies of active-session matching, fallback header identity,
274
+ or home-relative path formatting.
266
275
 
267
276
  ## Modularity contract
268
277
 
package/controller.d.ts CHANGED
@@ -72,6 +72,23 @@ export interface SessionInventoryProjectionOptions {
72
72
  isWritable?(descriptor: SessionDescriptor): boolean;
73
73
  }
74
74
 
75
+ export interface ActiveSessionRef {
76
+ harness: string;
77
+ sessionId: string | null;
78
+ }
79
+
80
+ export function formatWorkspacePath(cwd: string | null | undefined, home?: string): string;
81
+ export function matchesSessionRef(
82
+ descriptor: SessionDescriptor,
83
+ active: ActiveSessionRef | null | undefined,
84
+ ): boolean;
85
+ export function projectAttachedSession(
86
+ active: ActiveSessionRef | null | undefined,
87
+ rows: readonly SessionRowModel[],
88
+ fallbackWorkspace: string,
89
+ home?: string,
90
+ ): AttachedSessionModel | null;
91
+
75
92
  export function sessionConversationUpdatedAt(descriptor: SessionDescriptor): number | null;
76
93
  export function sessionDescriptorRuntimeStatus(
77
94
  descriptor: SessionDescriptor,
package/controller.mjs CHANGED
@@ -29,12 +29,42 @@ function workspaceName(cwd) {
29
29
  return cwd.replaceAll('\\', '/').split('/').filter(Boolean).at(-1) ?? cwd;
30
30
  }
31
31
 
32
- function shortWorkspacePath(cwd, home) {
32
+ export function formatWorkspacePath(cwd, home) {
33
33
  if (typeof cwd !== 'string' || !cwd) return '';
34
34
  const root = typeof home === 'string' && home.endsWith('/') ? home.slice(0, -1) : home;
35
35
  return root && (cwd === root || cwd.startsWith(`${root}/`)) ? `~${cwd.slice(root.length)}` : cwd;
36
36
  }
37
37
 
38
+ export function matchesSessionRef(descriptor, active) {
39
+ return Boolean(
40
+ active?.sessionId !== null &&
41
+ active?.sessionId !== undefined &&
42
+ descriptor?.locator?.harness === active.harness &&
43
+ descriptor?.locator?.session_id === active.sessionId,
44
+ );
45
+ }
46
+
47
+ export function projectAttachedSession(active, rows, fallbackWorkspace, home) {
48
+ if (!active?.harness) return null;
49
+ const row = rows.find((candidate) => candidate.active);
50
+ if (row) {
51
+ return {
52
+ key: row.key,
53
+ harness: row.harness,
54
+ name: row.name,
55
+ cwd: row.cwd,
56
+ title: row.title,
57
+ };
58
+ }
59
+ return {
60
+ key: '',
61
+ harness: active.harness,
62
+ name: workspaceName(fallbackWorkspace) || 'no workspace',
63
+ cwd: formatWorkspacePath(fallbackWorkspace, home),
64
+ title: workspaceName(fallbackWorkspace) || 'Untitled chat',
65
+ };
66
+ }
67
+
38
68
  function compactTopic(value) {
39
69
  const text = typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '';
40
70
  if (text.length <= 72) return text;
@@ -115,7 +145,7 @@ export function projectSessionInventory(descriptors, options) {
115
145
  key,
116
146
  harness: descriptor.locator.harness,
117
147
  name: workspaceName(descriptor.cwd) || 'no workspace',
118
- cwd: shortWorkspacePath(descriptor.cwd, options.home),
148
+ cwd: formatWorkspacePath(descriptor.cwd, options.home),
119
149
  title: descriptorTitle(descriptor),
120
150
  preview: preview?.text ?? '',
121
151
  age: relativeAge(preview?.updatedAt ?? updatedAt, now),
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.63",
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": {