@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 +9 -0
- package/controller.d.ts +17 -0
- package/controller.mjs +32 -2
- package/host.d.ts +37 -0
- package/host.mjs +241 -0
- 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
|
|
@@ -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
|
|
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:
|
|
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) {
|