@volter-ai-dev/supercode-ui 0.1.58 → 0.1.59
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 +36 -2
- package/controller.mjs +10 -4
- package/host.d.ts +103 -0
- package/host.mjs +384 -0
- package/package.json +7 -1
package/README.md
CHANGED
|
@@ -185,11 +185,45 @@ mounted.update(binding.getState());
|
|
|
185
185
|
```
|
|
186
186
|
|
|
187
187
|
The binding maps standard controller operations, including verified reduce-and-continue, and
|
|
188
|
-
projects its durable reduction receipt directly. Host-owned pagination, durable
|
|
189
|
-
drafts, and artifact materialization remain explicit callbacks.
|
|
188
|
+
projects its durable reduction receipt directly. Host-owned session-list pagination, durable
|
|
189
|
+
attention, drafts, and artifact materialization remain explicit callbacks. Transcript pagination
|
|
190
|
+
dispatches to the controller by default. A
|
|
190
191
|
browser should receive projected state from a trusted host rather than instantiate a local
|
|
191
192
|
controller or gain filesystem authority.
|
|
192
193
|
|
|
194
|
+
### Bind across a process boundary
|
|
195
|
+
|
|
196
|
+
Browser-hosted products use the transport-neutral host binding instead of recreating snapshot
|
|
197
|
+
ordering and intent dispatch around the same controller:
|
|
198
|
+
|
|
199
|
+
```js
|
|
200
|
+
import { createRemoteControllerHost, createRemoteUiBinding } from '@volter-ai-dev/supercode-ui/host';
|
|
201
|
+
|
|
202
|
+
// Trusted process
|
|
203
|
+
const host = createRemoteControllerHost(controller);
|
|
204
|
+
events.send(host.getFrame());
|
|
205
|
+
host.subscribe((frame) => events.send(frame));
|
|
206
|
+
http.onIntent((intent) => host.dispatch(intent));
|
|
207
|
+
|
|
208
|
+
// Browser
|
|
209
|
+
const binding = createRemoteUiBinding({
|
|
210
|
+
initialFrame,
|
|
211
|
+
dispatch: (intent) => http.postIntent(intent),
|
|
212
|
+
attention: {
|
|
213
|
+
state: loadAttention(),
|
|
214
|
+
onChange: saveAttention,
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
events.onFrame((frame) => binding.receive(frame));
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Each serializable frame carries a host-process identity, workspace generation, monotonic sequence,
|
|
221
|
+
controller revision, canonical UI state, and stable opaque reconnect identities. The browser store
|
|
222
|
+
rejects duplicate and stale frames, including delayed action responses from a retired host process.
|
|
223
|
+
The optional attention tracker baselines initial inventory without inventing unread dots, persists
|
|
224
|
+
by stable opaque identity, and marks only newer conversation evidence or a proven runtime completion.
|
|
225
|
+
HTTP, SSE, WebSocket, authentication, and product shell behavior remain host-owned transports.
|
|
226
|
+
|
|
193
227
|
Native continuation exposes one action and a quiet execution-transport selector. A host with a real
|
|
194
228
|
terminal provider can add `terminal` to `continuationModes` and handle `onResumeTerminal`; Terminal
|
|
195
229
|
is then the initial choice and Headless remains available from the selector. The UI never infers
|
package/controller.mjs
CHANGED
|
@@ -433,7 +433,7 @@ function attached(snapshot) {
|
|
|
433
433
|
harness: session.harness,
|
|
434
434
|
name: workspaceName(session.cwd),
|
|
435
435
|
cwd: session.cwd ?? '',
|
|
436
|
-
title: session.title ?? 'Untitled chat',
|
|
436
|
+
title: session.displayTitle ?? session.title ?? 'Untitled chat',
|
|
437
437
|
};
|
|
438
438
|
}
|
|
439
439
|
|
|
@@ -509,8 +509,10 @@ function projectClientSnapshotInternal(snapshot, options, imageRegistry) {
|
|
|
509
509
|
harness: session.harness,
|
|
510
510
|
name: workspaceName(session.cwd),
|
|
511
511
|
cwd: session.cwd ?? '',
|
|
512
|
-
title: session.title ?? 'Untitled chat',
|
|
513
|
-
|
|
512
|
+
title: session.displayTitle ?? session.title ?? 'Untitled chat',
|
|
513
|
+
preview: session.preview ?? '',
|
|
514
|
+
age: relativeAge(session.previewUpdatedAt ?? session.updatedAt, now),
|
|
515
|
+
previewUpdatedAt: session.previewUpdatedAt ?? null,
|
|
514
516
|
updatedAt: session.updatedAt ?? null,
|
|
515
517
|
messages: session.messageCount ?? null,
|
|
516
518
|
active: session.key === snapshot.activeSessionKey,
|
|
@@ -677,7 +679,11 @@ export async function dispatchControllerIntent(controller, intent, options = {})
|
|
|
677
679
|
if (intent.action === 'draft') return options.onDraft?.(intent.text);
|
|
678
680
|
if (intent.action === 'ack') return options.onAcknowledge?.(intent.key);
|
|
679
681
|
if (intent.action === 'loadSessions') return options.onLoadSessions?.();
|
|
680
|
-
if (intent.action === 'loadEarlier')
|
|
682
|
+
if (intent.action === 'loadEarlier') {
|
|
683
|
+
return options.onLoadEarlier
|
|
684
|
+
? options.onLoadEarlier()
|
|
685
|
+
: controller.dispatch({ type: 'loadEarlier' });
|
|
686
|
+
}
|
|
681
687
|
return options.onUnsupported?.(intent);
|
|
682
688
|
}
|
|
683
689
|
|
package/host.d.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { SupercodeClientSnapshot, SupercodeController } from '@volter-ai-dev/supercode-client';
|
|
2
|
+
import type { ClientProjectionOptions, ControllerBindingOptions } from './controller.js';
|
|
3
|
+
import type { SupercodeUiIntent, SupercodeUiState, UiAdapter } from './index.js';
|
|
4
|
+
|
|
5
|
+
export interface RemoteUiFrame {
|
|
6
|
+
schema: 'supercode.ui-host-state.v1';
|
|
7
|
+
hostInstanceId: string;
|
|
8
|
+
generation: number;
|
|
9
|
+
sequence: number;
|
|
10
|
+
controllerRevision: number;
|
|
11
|
+
workspace: string;
|
|
12
|
+
state: SupercodeUiState;
|
|
13
|
+
sessionIdentities: ReadonlyArray<{ key: string; identity: string }>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface RemoteControllerHostOptions extends Omit<ControllerBindingOptions, 'projection'> {
|
|
17
|
+
hostInstanceId?: string;
|
|
18
|
+
projection?(snapshot: SupercodeClientSnapshot): ClientProjectionOptions;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class RemoteControllerHost {
|
|
22
|
+
constructor(controller: SupercodeController, options?: RemoteControllerHostOptions);
|
|
23
|
+
close(): void;
|
|
24
|
+
dispatch(intent: unknown): Promise<RemoteUiFrame>;
|
|
25
|
+
getFrame(): RemoteUiFrame;
|
|
26
|
+
refreshProjection(): RemoteUiFrame;
|
|
27
|
+
restore(identity: string, connection: 'observe' | 'attach'): Promise<RemoteUiFrame>;
|
|
28
|
+
subscribe(listener: (frame: RemoteUiFrame) => void): () => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createRemoteControllerHost(
|
|
32
|
+
controller: SupercodeController,
|
|
33
|
+
options?: RemoteControllerHostOptions,
|
|
34
|
+
): RemoteControllerHost;
|
|
35
|
+
|
|
36
|
+
export function assertRemoteUiFrame(value: unknown): RemoteUiFrame;
|
|
37
|
+
|
|
38
|
+
export class RemoteUiStore {
|
|
39
|
+
constructor(initialFrame?: RemoteUiFrame | null);
|
|
40
|
+
getFrame(): RemoteUiFrame | null;
|
|
41
|
+
getState(): SupercodeUiState | null;
|
|
42
|
+
receive(frame: unknown): boolean;
|
|
43
|
+
subscribe(listener: (state: SupercodeUiState, frame: RemoteUiFrame) => void): () => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function createRemoteUiStore(initialFrame?: RemoteUiFrame | null): RemoteUiStore;
|
|
47
|
+
|
|
48
|
+
export interface SessionAttentionTrackerState {
|
|
49
|
+
version: 1;
|
|
50
|
+
sessions: Record<string, {
|
|
51
|
+
observedAt: number;
|
|
52
|
+
messages: number | null;
|
|
53
|
+
activity: import('./index.js').SessionActivity;
|
|
54
|
+
attention: import('./index.js').SessionAttention['kind'] | null;
|
|
55
|
+
}>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface SessionAttentionTrackerOptions {
|
|
59
|
+
state?: unknown;
|
|
60
|
+
onChange?(state: SessionAttentionTrackerState): void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class SessionAttentionTracker {
|
|
64
|
+
constructor(options?: SessionAttentionTrackerOptions);
|
|
65
|
+
acknowledge(frame: RemoteUiFrame, key: string): boolean;
|
|
66
|
+
attention(frame: RemoteUiFrame): import('./index.js').SessionAttention[];
|
|
67
|
+
observe(frame: RemoteUiFrame): import('./index.js').SessionAttention[];
|
|
68
|
+
snapshot(): SessionAttentionTrackerState;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function createSessionAttentionTracker(
|
|
72
|
+
options?: SessionAttentionTrackerOptions,
|
|
73
|
+
): SessionAttentionTracker;
|
|
74
|
+
|
|
75
|
+
export interface RemoteUiBindingOptions
|
|
76
|
+
extends Pick<
|
|
77
|
+
UiAdapter,
|
|
78
|
+
| 'confirmIntent'
|
|
79
|
+
| 'pickContext'
|
|
80
|
+
| 'resolveImage'
|
|
81
|
+
| 'openKeyboardShortcuts'
|
|
82
|
+
| 'onClose'
|
|
83
|
+
| 'onOpen'
|
|
84
|
+
| 'copyText'
|
|
85
|
+
| 'now'
|
|
86
|
+
> {
|
|
87
|
+
initialFrame?: RemoteUiFrame | null;
|
|
88
|
+
attention?: SessionAttentionTrackerOptions;
|
|
89
|
+
dispatch(intent: SupercodeUiIntent): Promise<RemoteUiFrame | null | void>;
|
|
90
|
+
handleIntent?(intent: SupercodeUiIntent): boolean | Promise<boolean>;
|
|
91
|
+
onAcknowledge?(key: string): void | Promise<void>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface RemoteUiBinding {
|
|
95
|
+
adapter: UiAdapter;
|
|
96
|
+
close(): void;
|
|
97
|
+
getFrame(): RemoteUiFrame | null;
|
|
98
|
+
getState(): SupercodeUiState | null;
|
|
99
|
+
receive(frame: unknown): boolean;
|
|
100
|
+
subscribe(listener: (state: SupercodeUiState, frame: RemoteUiFrame) => void): () => void;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function createRemoteUiBinding(options: RemoteUiBindingOptions): RemoteUiBinding;
|
package/host.mjs
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import { parseSupercodeUiIntent, sessionActivity } from './core.mjs';
|
|
2
|
+
import { dispatchControllerIntent, projectClientSnapshot } from './controller.mjs';
|
|
3
|
+
|
|
4
|
+
const FRAME_SCHEMA = 'supercode.ui-host-state.v1';
|
|
5
|
+
|
|
6
|
+
function objectRecord(value) {
|
|
7
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function defaultInstanceId() {
|
|
11
|
+
if (typeof globalThis.crypto?.randomUUID === 'function') return globalThis.crypto.randomUUID();
|
|
12
|
+
return `host-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Runtime guard for transport payloads received from a trusted Supercode host. */
|
|
16
|
+
export function assertRemoteUiFrame(value) {
|
|
17
|
+
const frame = objectRecord(value);
|
|
18
|
+
const state = objectRecord(frame?.state);
|
|
19
|
+
if (
|
|
20
|
+
frame?.schema !== FRAME_SCHEMA ||
|
|
21
|
+
typeof frame.hostInstanceId !== 'string' ||
|
|
22
|
+
!frame.hostInstanceId ||
|
|
23
|
+
!Number.isSafeInteger(frame.generation) ||
|
|
24
|
+
frame.generation < 1 ||
|
|
25
|
+
!Number.isSafeInteger(frame.sequence) ||
|
|
26
|
+
frame.sequence < 1 ||
|
|
27
|
+
!Number.isSafeInteger(frame.controllerRevision) ||
|
|
28
|
+
frame.controllerRevision < 0 ||
|
|
29
|
+
typeof frame.workspace !== 'string' ||
|
|
30
|
+
!state ||
|
|
31
|
+
!Array.isArray(state.sessions) ||
|
|
32
|
+
!Array.isArray(state.transcript) ||
|
|
33
|
+
!Array.isArray(frame.sessionIdentities) ||
|
|
34
|
+
!frame.sessionIdentities.every(
|
|
35
|
+
(item) =>
|
|
36
|
+
objectRecord(item) &&
|
|
37
|
+
typeof item.key === 'string' &&
|
|
38
|
+
typeof item.identity === 'string',
|
|
39
|
+
)
|
|
40
|
+
) {
|
|
41
|
+
throw new TypeError('Malformed Supercode remote UI frame.');
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Trusted-host binding that projects one ordered, serializable UI frame and
|
|
47
|
+
* dispatches the same typed intents as an in-process messenger. */
|
|
48
|
+
export class RemoteControllerHost {
|
|
49
|
+
#bindingOptions;
|
|
50
|
+
#controller;
|
|
51
|
+
#frame;
|
|
52
|
+
#generation = 0;
|
|
53
|
+
#hostInstanceId;
|
|
54
|
+
#lastControllerRevision = -1;
|
|
55
|
+
#listeners = new Set();
|
|
56
|
+
#sequence = 0;
|
|
57
|
+
#unsubscribe;
|
|
58
|
+
#workspace = null;
|
|
59
|
+
|
|
60
|
+
constructor(controller, options = {}) {
|
|
61
|
+
if (
|
|
62
|
+
!controller ||
|
|
63
|
+
typeof controller.getSnapshot !== 'function' ||
|
|
64
|
+
typeof controller.subscribe !== 'function' ||
|
|
65
|
+
typeof controller.dispatch !== 'function'
|
|
66
|
+
) {
|
|
67
|
+
throw new TypeError('RemoteControllerHost requires a SupercodeController-compatible object.');
|
|
68
|
+
}
|
|
69
|
+
this.#controller = controller;
|
|
70
|
+
this.#hostInstanceId = options.hostInstanceId ?? defaultInstanceId();
|
|
71
|
+
if (typeof this.#hostInstanceId !== 'string' || !this.#hostInstanceId) {
|
|
72
|
+
throw new TypeError('RemoteControllerHost hostInstanceId must be a non-empty string.');
|
|
73
|
+
}
|
|
74
|
+
this.#bindingOptions = options;
|
|
75
|
+
this.#capture(true);
|
|
76
|
+
this.#unsubscribe = controller.subscribe(() => this.#capture(false));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
close() {
|
|
80
|
+
this.#unsubscribe?.();
|
|
81
|
+
this.#unsubscribe = null;
|
|
82
|
+
this.#listeners.clear();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
getFrame() {
|
|
86
|
+
return this.#frame;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
subscribe(listener) {
|
|
90
|
+
this.#listeners.add(listener);
|
|
91
|
+
listener(this.#frame);
|
|
92
|
+
return () => this.#listeners.delete(listener);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Reproject host-owned overlays such as machine-wide inventory or durable attention. */
|
|
96
|
+
refreshProjection() {
|
|
97
|
+
this.#capture(true);
|
|
98
|
+
return this.#frame;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async dispatch(value) {
|
|
102
|
+
const intent = parseSupercodeUiIntent(value);
|
|
103
|
+
if (!intent) throw new TypeError('Invalid Supercode UI intent.');
|
|
104
|
+
const sequence = this.#sequence;
|
|
105
|
+
await dispatchControllerIntent(this.#controller, intent, this.#bindingOptions);
|
|
106
|
+
if (this.#sequence === sequence) this.#capture(true);
|
|
107
|
+
return this.#frame;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async restore(identity, connection) {
|
|
111
|
+
if (typeof identity !== 'string' || !identity) {
|
|
112
|
+
throw new TypeError('RemoteControllerHost restore identity must be a non-empty string.');
|
|
113
|
+
}
|
|
114
|
+
if (connection !== 'observe' && connection !== 'attach') {
|
|
115
|
+
throw new TypeError('RemoteControllerHost restore connection must be observe or attach.');
|
|
116
|
+
}
|
|
117
|
+
const sequence = this.#sequence;
|
|
118
|
+
await this.#controller.dispatch({ type: 'restore', identity, connection });
|
|
119
|
+
if (this.#sequence === sequence) this.#capture(true);
|
|
120
|
+
return this.#frame;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#capture(force) {
|
|
124
|
+
const snapshot = this.#controller.getSnapshot();
|
|
125
|
+
if (snapshot.workspace !== this.#workspace) {
|
|
126
|
+
this.#workspace = snapshot.workspace;
|
|
127
|
+
this.#generation += 1;
|
|
128
|
+
this.#lastControllerRevision = -1;
|
|
129
|
+
force = true;
|
|
130
|
+
}
|
|
131
|
+
if (!force && snapshot.revision === this.#lastControllerRevision) return;
|
|
132
|
+
this.#lastControllerRevision = snapshot.revision;
|
|
133
|
+
this.#sequence += 1;
|
|
134
|
+
const projection = this.#bindingOptions.projection?.(snapshot) ?? {};
|
|
135
|
+
this.#frame = Object.freeze({
|
|
136
|
+
schema: FRAME_SCHEMA,
|
|
137
|
+
hostInstanceId: this.#hostInstanceId,
|
|
138
|
+
generation: this.#generation,
|
|
139
|
+
sequence: this.#sequence,
|
|
140
|
+
controllerRevision: snapshot.revision,
|
|
141
|
+
workspace: snapshot.workspace,
|
|
142
|
+
state: projectClientSnapshot(snapshot, projection),
|
|
143
|
+
sessionIdentities: Object.freeze(
|
|
144
|
+
snapshot.sessions.map((session) =>
|
|
145
|
+
Object.freeze({ key: session.key, identity: session.identity }),
|
|
146
|
+
),
|
|
147
|
+
),
|
|
148
|
+
});
|
|
149
|
+
for (const listener of this.#listeners) listener(this.#frame);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function createRemoteControllerHost(controller, options) {
|
|
154
|
+
return new RemoteControllerHost(controller, options);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Monotonic browser-side store. Once a newer host process is accepted, frames
|
|
158
|
+
* from every retired process are rejected permanently. */
|
|
159
|
+
export class RemoteUiStore {
|
|
160
|
+
#frame = null;
|
|
161
|
+
#listeners = new Set();
|
|
162
|
+
#retiredHostInstances = new Set();
|
|
163
|
+
|
|
164
|
+
constructor(initialFrame) {
|
|
165
|
+
if (initialFrame !== undefined && initialFrame !== null) this.receive(initialFrame);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
getFrame() {
|
|
169
|
+
return this.#frame;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
getState() {
|
|
173
|
+
return this.#frame?.state ?? null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
subscribe(listener) {
|
|
177
|
+
this.#listeners.add(listener);
|
|
178
|
+
if (this.#frame) listener(this.#frame.state, this.#frame);
|
|
179
|
+
return () => this.#listeners.delete(listener);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
receive(value) {
|
|
183
|
+
const next = assertRemoteUiFrame(value);
|
|
184
|
+
const current = this.#frame;
|
|
185
|
+
if (current) {
|
|
186
|
+
if (next.hostInstanceId !== current.hostInstanceId) {
|
|
187
|
+
if (this.#retiredHostInstances.has(next.hostInstanceId)) return false;
|
|
188
|
+
this.#retiredHostInstances.add(current.hostInstanceId);
|
|
189
|
+
} else if (
|
|
190
|
+
next.generation < current.generation ||
|
|
191
|
+
(next.generation === current.generation && next.sequence <= current.sequence)
|
|
192
|
+
) {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
this.#frame = next;
|
|
197
|
+
for (const listener of this.#listeners) listener(next.state, next);
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function createRemoteUiStore(initialFrame) {
|
|
203
|
+
return new RemoteUiStore(initialFrame);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function attentionState(value) {
|
|
207
|
+
const parsed = objectRecord(value);
|
|
208
|
+
const sessions = objectRecord(parsed?.sessions);
|
|
209
|
+
return {
|
|
210
|
+
version: 1,
|
|
211
|
+
sessions: sessions
|
|
212
|
+
? Object.fromEntries(
|
|
213
|
+
Object.entries(sessions).flatMap(([identity, record]) => {
|
|
214
|
+
const item = objectRecord(record);
|
|
215
|
+
if (!item) return [];
|
|
216
|
+
return [[identity, {
|
|
217
|
+
observedAt: Number.isFinite(item.observedAt) ? item.observedAt : 0,
|
|
218
|
+
messages: Number.isFinite(item.messages) ? item.messages : null,
|
|
219
|
+
activity: typeof item.activity === 'string' ? item.activity : 'idle',
|
|
220
|
+
attention: ['unseen', 'finished', 'failed'].includes(item.attention)
|
|
221
|
+
? item.attention
|
|
222
|
+
: null,
|
|
223
|
+
}]];
|
|
224
|
+
}),
|
|
225
|
+
)
|
|
226
|
+
: {},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function activeActivity(value) {
|
|
231
|
+
return value === 'running' || value === 'working' || value === 'needs-input';
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Reference durable attention reducer. It baselines initial inventory without
|
|
235
|
+
* manufacturing unread state, then records only newer conversation or runtime evidence. */
|
|
236
|
+
export class SessionAttentionTracker {
|
|
237
|
+
#onChange;
|
|
238
|
+
#state;
|
|
239
|
+
|
|
240
|
+
constructor(options = {}) {
|
|
241
|
+
this.#state = attentionState(options.state);
|
|
242
|
+
this.#onChange = options.onChange;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
observe(frameValue) {
|
|
246
|
+
const frame = assertRemoteUiFrame(frameValue);
|
|
247
|
+
const identities = new Map(frame.sessionIdentities.map((item) => [item.key, item.identity]));
|
|
248
|
+
let changed = false;
|
|
249
|
+
for (const row of frame.state.sessions) {
|
|
250
|
+
const identity = identities.get(row.key);
|
|
251
|
+
if (!identity) continue;
|
|
252
|
+
const observedAt = row.previewUpdatedAt ?? row.updatedAt ?? 0;
|
|
253
|
+
const messages = Number.isFinite(row.messages) ? row.messages : null;
|
|
254
|
+
const activity = sessionActivity(frame.state, row);
|
|
255
|
+
const prior = this.#state.sessions[identity];
|
|
256
|
+
if (!prior) {
|
|
257
|
+
this.#state.sessions[identity] = {
|
|
258
|
+
observedAt,
|
|
259
|
+
messages,
|
|
260
|
+
activity,
|
|
261
|
+
attention: null,
|
|
262
|
+
};
|
|
263
|
+
changed = true;
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const newerConversation =
|
|
267
|
+
observedAt > prior.observedAt ||
|
|
268
|
+
(messages !== null && prior.messages !== null && messages > prior.messages);
|
|
269
|
+
let attention = prior.attention;
|
|
270
|
+
if (newerConversation) attention = 'unseen';
|
|
271
|
+
if (activeActivity(prior.activity) && !activeActivity(activity)) {
|
|
272
|
+
attention = frame.state.error ? 'failed' : 'finished';
|
|
273
|
+
}
|
|
274
|
+
const next = {
|
|
275
|
+
observedAt: Math.max(prior.observedAt, observedAt),
|
|
276
|
+
messages:
|
|
277
|
+
messages === null
|
|
278
|
+
? prior.messages
|
|
279
|
+
: prior.messages === null
|
|
280
|
+
? messages
|
|
281
|
+
: Math.max(prior.messages, messages),
|
|
282
|
+
activity,
|
|
283
|
+
attention,
|
|
284
|
+
};
|
|
285
|
+
if (JSON.stringify(next) !== JSON.stringify(prior)) {
|
|
286
|
+
this.#state.sessions[identity] = next;
|
|
287
|
+
changed = true;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (changed) this.#emit();
|
|
291
|
+
return this.attention(frame);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
attention(frameValue) {
|
|
295
|
+
const frame = assertRemoteUiFrame(frameValue);
|
|
296
|
+
return frame.sessionIdentities.flatMap(({ key, identity }) => {
|
|
297
|
+
const attention = this.#state.sessions[identity]?.attention;
|
|
298
|
+
return attention ? [{ key, kind: attention }] : [];
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
acknowledge(frameValue, key) {
|
|
303
|
+
const frame = assertRemoteUiFrame(frameValue);
|
|
304
|
+
const identity = frame.sessionIdentities.find((item) => item.key === key)?.identity;
|
|
305
|
+
if (!identity || !this.#state.sessions[identity]?.attention) return false;
|
|
306
|
+
this.#state.sessions[identity].attention = null;
|
|
307
|
+
this.#emit();
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
snapshot() {
|
|
312
|
+
return structuredClone(this.#state);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
#emit() {
|
|
316
|
+
this.#onChange?.(this.snapshot());
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function createSessionAttentionTracker(options) {
|
|
321
|
+
return new SessionAttentionTracker(options);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Browser binding for any transport that can post one intent and return an
|
|
325
|
+
* optional authoritative frame. Local host actions may intercept an intent. */
|
|
326
|
+
export function createRemoteUiBinding(options) {
|
|
327
|
+
if (!options || typeof options.dispatch !== 'function') {
|
|
328
|
+
throw new TypeError('createRemoteUiBinding requires a dispatch function.');
|
|
329
|
+
}
|
|
330
|
+
const store = new RemoteUiStore(options.initialFrame);
|
|
331
|
+
const attention = options.attention
|
|
332
|
+
? new SessionAttentionTracker(options.attention)
|
|
333
|
+
: null;
|
|
334
|
+
let projectedState = null;
|
|
335
|
+
const listeners = new Set();
|
|
336
|
+
const project = (frame) => {
|
|
337
|
+
projectedState = attention
|
|
338
|
+
? { ...frame.state, attention: attention.observe(frame) }
|
|
339
|
+
: frame.state;
|
|
340
|
+
for (const listener of listeners) listener(projectedState, frame);
|
|
341
|
+
};
|
|
342
|
+
const unsubscribeStore = store.subscribe((_state, frame) => project(frame));
|
|
343
|
+
const adapter = {
|
|
344
|
+
async onIntent(value) {
|
|
345
|
+
const intent = parseSupercodeUiIntent(value);
|
|
346
|
+
if (!intent) throw new TypeError('Invalid Supercode UI intent.');
|
|
347
|
+
if (intent.action === 'ack' && attention) {
|
|
348
|
+
const frame = store.getFrame();
|
|
349
|
+
if (frame && attention.acknowledge(frame, intent.key)) project(frame);
|
|
350
|
+
await options.onAcknowledge?.(intent.key);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (await options.handleIntent?.(intent)) return;
|
|
354
|
+
const frame = await options.dispatch(intent);
|
|
355
|
+
if (frame) store.receive(frame);
|
|
356
|
+
},
|
|
357
|
+
...(options.confirmIntent ? { confirmIntent: options.confirmIntent } : {}),
|
|
358
|
+
...(options.pickContext ? { pickContext: options.pickContext } : {}),
|
|
359
|
+
...(options.resolveImage ? { resolveImage: options.resolveImage } : {}),
|
|
360
|
+
...(options.openKeyboardShortcuts
|
|
361
|
+
? { openKeyboardShortcuts: options.openKeyboardShortcuts }
|
|
362
|
+
: {}),
|
|
363
|
+
...(options.onClose ? { onClose: options.onClose } : {}),
|
|
364
|
+
...(options.onOpen ? { onOpen: options.onOpen } : {}),
|
|
365
|
+
...(options.copyText ? { copyText: options.copyText } : {}),
|
|
366
|
+
...(options.now ? { now: options.now } : {}),
|
|
367
|
+
};
|
|
368
|
+
return {
|
|
369
|
+
adapter,
|
|
370
|
+
getFrame: () => store.getFrame(),
|
|
371
|
+
getState: () => projectedState ?? store.getState(),
|
|
372
|
+
receive: (frame) => store.receive(frame),
|
|
373
|
+
subscribe(listener) {
|
|
374
|
+
listeners.add(listener);
|
|
375
|
+
const frame = store.getFrame();
|
|
376
|
+
if (frame && projectedState) listener(projectedState, frame);
|
|
377
|
+
return () => listeners.delete(listener);
|
|
378
|
+
},
|
|
379
|
+
close() {
|
|
380
|
+
unsubscribeStore();
|
|
381
|
+
listeners.clear();
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter-ai-dev/supercode-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.59",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Composable default UI kit for Supercode-powered coding-agent experiences",
|
|
6
6
|
"exports": {
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
"types": "./controller.d.ts",
|
|
17
17
|
"import": "./controller.mjs"
|
|
18
18
|
},
|
|
19
|
+
"./host": {
|
|
20
|
+
"types": "./host.d.ts",
|
|
21
|
+
"import": "./host.mjs"
|
|
22
|
+
},
|
|
19
23
|
"./preact": {
|
|
20
24
|
"types": "./components.d.ts",
|
|
21
25
|
"import": "./components.mjs"
|
|
@@ -108,6 +112,8 @@
|
|
|
108
112
|
"core.d.ts",
|
|
109
113
|
"controller.mjs",
|
|
110
114
|
"controller.d.ts",
|
|
115
|
+
"host.mjs",
|
|
116
|
+
"host.d.ts",
|
|
111
117
|
"components.mjs",
|
|
112
118
|
"components.d.ts",
|
|
113
119
|
"activity.mjs",
|