@volter-ai-dev/supercode-ui 0.1.57 → 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 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 attention,
189
- drafts, and artifact materialization remain explicit callbacks. A
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/components.mjs CHANGED
@@ -2345,31 +2345,32 @@ function SessionRow({ row, state, onOpen, onOpenSubagents, now = Date.now() }) {
2345
2345
  /* @__PURE__ */ jsxs7("button", { className: "scui-session-main", "data-session-key": row.key, type: "button", "aria-label": `${title} \xB7 ${harnessDisplayName(row.harness)}${row.writable ? "" : " \xB7 Read-only"}${working ? " \xB7 Working" : ""}${path.complete ? ` \xB7 ${path.complete}` : ""}${preview ? ` \xB7 ${preview}` : ""}${unreadCount ? ` \xB7 ${unreadCount} unread` : ""}${age ? ` \xB7 ${age}` : ""}`, "aria-current": row.active ? "true" : void 0, onClick: () => onOpen(row), children: [
2346
2346
  /* @__PURE__ */ jsx8(HarnessLogo, { id: row.harness, activity, size: 34 }),
2347
2347
  /* @__PURE__ */ jsxs7("span", { className: "scui-session-copy", children: [
2348
- /* @__PURE__ */ jsxs7("span", { className: "scui-session-title", children: [
2349
- /* @__PURE__ */ jsx8("strong", { children: title }),
2350
- /* @__PURE__ */ jsx8("span", { className: "scui-session-meta", children: age ? /* @__PURE__ */ jsx8("time", { children: age }) : null })
2351
- ] }),
2348
+ /* @__PURE__ */ jsx8("span", { className: "scui-session-title", children: /* @__PURE__ */ jsx8("strong", { children: title }) }),
2352
2349
  path.complete ? /* @__PURE__ */ jsxs7("small", { className: "scui-session-path", title: row.cwd, children: [
2353
2350
  /* @__PURE__ */ jsx8("span", { className: "scui-session-path-leading", children: path.leading }),
2354
2351
  path.separator ? /* @__PURE__ */ jsx8("span", { className: "scui-session-path-separator", children: path.separator }) : null,
2355
2352
  path.trailing ? /* @__PURE__ */ jsx8("span", { className: "scui-session-path-trailing", children: path.trailing }) : null
2356
2353
  ] }) : null,
2357
- working || preview || unreadCount ? /* @__PURE__ */ jsxs7("span", { className: "scui-session-preview", children: [
2354
+ working || preview ? /* @__PURE__ */ jsxs7("span", { className: "scui-session-preview", children: [
2358
2355
  working ? /* @__PURE__ */ jsxs7("span", { className: "scui-session-working", "aria-hidden": "true", children: [
2359
2356
  /* @__PURE__ */ jsx8("i", {}),
2360
2357
  /* @__PURE__ */ jsx8("i", {}),
2361
2358
  /* @__PURE__ */ jsx8("i", {})
2362
2359
  ] }) : null,
2363
- preview || working ? /* @__PURE__ */ jsx8("small", { children: preview || "Working\u2026" }) : null,
2364
- unreadCount ? /* @__PURE__ */ jsx8("b", { "aria-label": `${unreadCount} unread messages`, children: unreadLabel }) : null
2360
+ preview || working ? /* @__PURE__ */ jsx8("small", { children: preview || "Working\u2026" }) : null
2365
2361
  ] }) : null,
2366
2362
  state.attachError?.key === row.key ? /* @__PURE__ */ jsx8("em", { children: state.attachError.message }) : null
2367
2363
  ] })
2368
2364
  ] }),
2369
- row.subagentCount ? /* @__PURE__ */ jsxs7("button", { className: "scui-session-agents", type: "button", "aria-label": `Open ${row.subagentCount} agent sessions in ${title}`, onClick: () => onOpenSubagents?.(row), children: [
2370
- /* @__PURE__ */ jsx8(UiIcon, { name: "agents", size: 14 }),
2371
- "Agents \xB7 ",
2372
- row.subagentCount
2365
+ row.subagentCount || age || unreadCount ? /* @__PURE__ */ jsxs7("span", { className: "scui-session-rail", children: [
2366
+ /* @__PURE__ */ jsxs7("span", { className: "scui-session-rail-meta", children: [
2367
+ row.subagentCount ? /* @__PURE__ */ jsxs7("button", { className: "scui-session-agents", type: "button", title: `${row.subagentCount} agent sessions`, "aria-label": `Open ${row.subagentCount} agent sessions in ${title}`, onClick: () => onOpenSubagents?.(row), children: [
2368
+ /* @__PURE__ */ jsx8(UiIcon, { name: "agents", size: 14 }),
2369
+ /* @__PURE__ */ jsx8("span", { children: row.subagentCount })
2370
+ ] }) : null,
2371
+ age ? /* @__PURE__ */ jsx8("time", { children: age }) : null
2372
+ ] }),
2373
+ unreadCount ? /* @__PURE__ */ jsx8("b", { "aria-label": `${unreadCount} unread messages`, children: unreadLabel }) : null
2373
2374
  ] }) : null
2374
2375
  ] });
2375
2376
  }
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
- age: relativeAge(session.updatedAt, now),
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') return options.onLoadEarlier?.();
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/embed.mjs CHANGED
@@ -2262,31 +2262,32 @@ function SessionRow({ row, state, onOpen, onOpenSubagents, now = Date.now() }) {
2262
2262
  /* @__PURE__ */ jsxs6("button", { className: "scui-session-main", "data-session-key": row.key, type: "button", "aria-label": `${title} \xB7 ${harnessDisplayName(row.harness)}${row.writable ? "" : " \xB7 Read-only"}${working ? " \xB7 Working" : ""}${path.complete ? ` \xB7 ${path.complete}` : ""}${preview ? ` \xB7 ${preview}` : ""}${unreadCount ? ` \xB7 ${unreadCount} unread` : ""}${age ? ` \xB7 ${age}` : ""}`, "aria-current": row.active ? "true" : void 0, onClick: () => onOpen(row), children: [
2263
2263
  /* @__PURE__ */ jsx7(HarnessLogo, { id: row.harness, activity, size: 34 }),
2264
2264
  /* @__PURE__ */ jsxs6("span", { className: "scui-session-copy", children: [
2265
- /* @__PURE__ */ jsxs6("span", { className: "scui-session-title", children: [
2266
- /* @__PURE__ */ jsx7("strong", { children: title }),
2267
- /* @__PURE__ */ jsx7("span", { className: "scui-session-meta", children: age ? /* @__PURE__ */ jsx7("time", { children: age }) : null })
2268
- ] }),
2265
+ /* @__PURE__ */ jsx7("span", { className: "scui-session-title", children: /* @__PURE__ */ jsx7("strong", { children: title }) }),
2269
2266
  path.complete ? /* @__PURE__ */ jsxs6("small", { className: "scui-session-path", title: row.cwd, children: [
2270
2267
  /* @__PURE__ */ jsx7("span", { className: "scui-session-path-leading", children: path.leading }),
2271
2268
  path.separator ? /* @__PURE__ */ jsx7("span", { className: "scui-session-path-separator", children: path.separator }) : null,
2272
2269
  path.trailing ? /* @__PURE__ */ jsx7("span", { className: "scui-session-path-trailing", children: path.trailing }) : null
2273
2270
  ] }) : null,
2274
- working || preview || unreadCount ? /* @__PURE__ */ jsxs6("span", { className: "scui-session-preview", children: [
2271
+ working || preview ? /* @__PURE__ */ jsxs6("span", { className: "scui-session-preview", children: [
2275
2272
  working ? /* @__PURE__ */ jsxs6("span", { className: "scui-session-working", "aria-hidden": "true", children: [
2276
2273
  /* @__PURE__ */ jsx7("i", {}),
2277
2274
  /* @__PURE__ */ jsx7("i", {}),
2278
2275
  /* @__PURE__ */ jsx7("i", {})
2279
2276
  ] }) : null,
2280
- preview || working ? /* @__PURE__ */ jsx7("small", { children: preview || "Working\u2026" }) : null,
2281
- unreadCount ? /* @__PURE__ */ jsx7("b", { "aria-label": `${unreadCount} unread messages`, children: unreadLabel }) : null
2277
+ preview || working ? /* @__PURE__ */ jsx7("small", { children: preview || "Working\u2026" }) : null
2282
2278
  ] }) : null,
2283
2279
  state.attachError?.key === row.key ? /* @__PURE__ */ jsx7("em", { children: state.attachError.message }) : null
2284
2280
  ] })
2285
2281
  ] }),
2286
- row.subagentCount ? /* @__PURE__ */ jsxs6("button", { className: "scui-session-agents", type: "button", "aria-label": `Open ${row.subagentCount} agent sessions in ${title}`, onClick: () => onOpenSubagents?.(row), children: [
2287
- /* @__PURE__ */ jsx7(UiIcon, { name: "agents", size: 14 }),
2288
- "Agents \xB7 ",
2289
- row.subagentCount
2282
+ row.subagentCount || age || unreadCount ? /* @__PURE__ */ jsxs6("span", { className: "scui-session-rail", children: [
2283
+ /* @__PURE__ */ jsxs6("span", { className: "scui-session-rail-meta", children: [
2284
+ row.subagentCount ? /* @__PURE__ */ jsxs6("button", { className: "scui-session-agents", type: "button", title: `${row.subagentCount} agent sessions`, "aria-label": `Open ${row.subagentCount} agent sessions in ${title}`, onClick: () => onOpenSubagents?.(row), children: [
2285
+ /* @__PURE__ */ jsx7(UiIcon, { name: "agents", size: 14 }),
2286
+ /* @__PURE__ */ jsx7("span", { children: row.subagentCount })
2287
+ ] }) : null,
2288
+ age ? /* @__PURE__ */ jsx7("time", { children: age }) : null
2289
+ ] }),
2290
+ unreadCount ? /* @__PURE__ */ jsx7("b", { "aria-label": `${unreadCount} unread messages`, children: unreadLabel }) : null
2290
2291
  ] }) : null
2291
2292
  ] });
2292
2293
  }
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/messenger.mjs CHANGED
@@ -2259,31 +2259,32 @@ function SessionRow({ row, state, onOpen, onOpenSubagents, now = Date.now() }) {
2259
2259
  /* @__PURE__ */ jsxs6("button", { className: "scui-session-main", "data-session-key": row.key, type: "button", "aria-label": `${title} \xB7 ${harnessDisplayName(row.harness)}${row.writable ? "" : " \xB7 Read-only"}${working ? " \xB7 Working" : ""}${path.complete ? ` \xB7 ${path.complete}` : ""}${preview ? ` \xB7 ${preview}` : ""}${unreadCount ? ` \xB7 ${unreadCount} unread` : ""}${age ? ` \xB7 ${age}` : ""}`, "aria-current": row.active ? "true" : void 0, onClick: () => onOpen(row), children: [
2260
2260
  /* @__PURE__ */ jsx7(HarnessLogo, { id: row.harness, activity, size: 34 }),
2261
2261
  /* @__PURE__ */ jsxs6("span", { className: "scui-session-copy", children: [
2262
- /* @__PURE__ */ jsxs6("span", { className: "scui-session-title", children: [
2263
- /* @__PURE__ */ jsx7("strong", { children: title }),
2264
- /* @__PURE__ */ jsx7("span", { className: "scui-session-meta", children: age ? /* @__PURE__ */ jsx7("time", { children: age }) : null })
2265
- ] }),
2262
+ /* @__PURE__ */ jsx7("span", { className: "scui-session-title", children: /* @__PURE__ */ jsx7("strong", { children: title }) }),
2266
2263
  path.complete ? /* @__PURE__ */ jsxs6("small", { className: "scui-session-path", title: row.cwd, children: [
2267
2264
  /* @__PURE__ */ jsx7("span", { className: "scui-session-path-leading", children: path.leading }),
2268
2265
  path.separator ? /* @__PURE__ */ jsx7("span", { className: "scui-session-path-separator", children: path.separator }) : null,
2269
2266
  path.trailing ? /* @__PURE__ */ jsx7("span", { className: "scui-session-path-trailing", children: path.trailing }) : null
2270
2267
  ] }) : null,
2271
- working || preview || unreadCount ? /* @__PURE__ */ jsxs6("span", { className: "scui-session-preview", children: [
2268
+ working || preview ? /* @__PURE__ */ jsxs6("span", { className: "scui-session-preview", children: [
2272
2269
  working ? /* @__PURE__ */ jsxs6("span", { className: "scui-session-working", "aria-hidden": "true", children: [
2273
2270
  /* @__PURE__ */ jsx7("i", {}),
2274
2271
  /* @__PURE__ */ jsx7("i", {}),
2275
2272
  /* @__PURE__ */ jsx7("i", {})
2276
2273
  ] }) : null,
2277
- preview || working ? /* @__PURE__ */ jsx7("small", { children: preview || "Working\u2026" }) : null,
2278
- unreadCount ? /* @__PURE__ */ jsx7("b", { "aria-label": `${unreadCount} unread messages`, children: unreadLabel }) : null
2274
+ preview || working ? /* @__PURE__ */ jsx7("small", { children: preview || "Working\u2026" }) : null
2279
2275
  ] }) : null,
2280
2276
  state.attachError?.key === row.key ? /* @__PURE__ */ jsx7("em", { children: state.attachError.message }) : null
2281
2277
  ] })
2282
2278
  ] }),
2283
- row.subagentCount ? /* @__PURE__ */ jsxs6("button", { className: "scui-session-agents", type: "button", "aria-label": `Open ${row.subagentCount} agent sessions in ${title}`, onClick: () => onOpenSubagents?.(row), children: [
2284
- /* @__PURE__ */ jsx7(UiIcon, { name: "agents", size: 14 }),
2285
- "Agents \xB7 ",
2286
- row.subagentCount
2279
+ row.subagentCount || age || unreadCount ? /* @__PURE__ */ jsxs6("span", { className: "scui-session-rail", children: [
2280
+ /* @__PURE__ */ jsxs6("span", { className: "scui-session-rail-meta", children: [
2281
+ row.subagentCount ? /* @__PURE__ */ jsxs6("button", { className: "scui-session-agents", type: "button", title: `${row.subagentCount} agent sessions`, "aria-label": `Open ${row.subagentCount} agent sessions in ${title}`, onClick: () => onOpenSubagents?.(row), children: [
2282
+ /* @__PURE__ */ jsx7(UiIcon, { name: "agents", size: 14 }),
2283
+ /* @__PURE__ */ jsx7("span", { children: row.subagentCount })
2284
+ ] }) : null,
2285
+ age ? /* @__PURE__ */ jsx7("time", { children: age }) : null
2286
+ ] }),
2287
+ unreadCount ? /* @__PURE__ */ jsx7("b", { "aria-label": `${unreadCount} unread messages`, children: unreadLabel }) : null
2287
2288
  ] }) : null
2288
2289
  ] });
2289
2290
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter-ai-dev/supercode-ui",
3
- "version": "0.1.57",
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",
@@ -2345,31 +2345,32 @@ function SessionRow({ row, state, onOpen, onOpenSubagents, now = Date.now() }) {
2345
2345
  /* @__PURE__ */ jsxs7("button", { className: "scui-session-main", "data-session-key": row.key, type: "button", "aria-label": `${title} \xB7 ${harnessDisplayName(row.harness)}${row.writable ? "" : " \xB7 Read-only"}${working ? " \xB7 Working" : ""}${path.complete ? ` \xB7 ${path.complete}` : ""}${preview ? ` \xB7 ${preview}` : ""}${unreadCount ? ` \xB7 ${unreadCount} unread` : ""}${age ? ` \xB7 ${age}` : ""}`, "aria-current": row.active ? "true" : void 0, onClick: () => onOpen(row), children: [
2346
2346
  /* @__PURE__ */ jsx8(HarnessLogo, { id: row.harness, activity, size: 34 }),
2347
2347
  /* @__PURE__ */ jsxs7("span", { className: "scui-session-copy", children: [
2348
- /* @__PURE__ */ jsxs7("span", { className: "scui-session-title", children: [
2349
- /* @__PURE__ */ jsx8("strong", { children: title }),
2350
- /* @__PURE__ */ jsx8("span", { className: "scui-session-meta", children: age ? /* @__PURE__ */ jsx8("time", { children: age }) : null })
2351
- ] }),
2348
+ /* @__PURE__ */ jsx8("span", { className: "scui-session-title", children: /* @__PURE__ */ jsx8("strong", { children: title }) }),
2352
2349
  path.complete ? /* @__PURE__ */ jsxs7("small", { className: "scui-session-path", title: row.cwd, children: [
2353
2350
  /* @__PURE__ */ jsx8("span", { className: "scui-session-path-leading", children: path.leading }),
2354
2351
  path.separator ? /* @__PURE__ */ jsx8("span", { className: "scui-session-path-separator", children: path.separator }) : null,
2355
2352
  path.trailing ? /* @__PURE__ */ jsx8("span", { className: "scui-session-path-trailing", children: path.trailing }) : null
2356
2353
  ] }) : null,
2357
- working || preview || unreadCount ? /* @__PURE__ */ jsxs7("span", { className: "scui-session-preview", children: [
2354
+ working || preview ? /* @__PURE__ */ jsxs7("span", { className: "scui-session-preview", children: [
2358
2355
  working ? /* @__PURE__ */ jsxs7("span", { className: "scui-session-working", "aria-hidden": "true", children: [
2359
2356
  /* @__PURE__ */ jsx8("i", {}),
2360
2357
  /* @__PURE__ */ jsx8("i", {}),
2361
2358
  /* @__PURE__ */ jsx8("i", {})
2362
2359
  ] }) : null,
2363
- preview || working ? /* @__PURE__ */ jsx8("small", { children: preview || "Working\u2026" }) : null,
2364
- unreadCount ? /* @__PURE__ */ jsx8("b", { "aria-label": `${unreadCount} unread messages`, children: unreadLabel }) : null
2360
+ preview || working ? /* @__PURE__ */ jsx8("small", { children: preview || "Working\u2026" }) : null
2365
2361
  ] }) : null,
2366
2362
  state.attachError?.key === row.key ? /* @__PURE__ */ jsx8("em", { children: state.attachError.message }) : null
2367
2363
  ] })
2368
2364
  ] }),
2369
- row.subagentCount ? /* @__PURE__ */ jsxs7("button", { className: "scui-session-agents", type: "button", "aria-label": `Open ${row.subagentCount} agent sessions in ${title}`, onClick: () => onOpenSubagents?.(row), children: [
2370
- /* @__PURE__ */ jsx8(UiIcon, { name: "agents", size: 14 }),
2371
- "Agents \xB7 ",
2372
- row.subagentCount
2365
+ row.subagentCount || age || unreadCount ? /* @__PURE__ */ jsxs7("span", { className: "scui-session-rail", children: [
2366
+ /* @__PURE__ */ jsxs7("span", { className: "scui-session-rail-meta", children: [
2367
+ row.subagentCount ? /* @__PURE__ */ jsxs7("button", { className: "scui-session-agents", type: "button", title: `${row.subagentCount} agent sessions`, "aria-label": `Open ${row.subagentCount} agent sessions in ${title}`, onClick: () => onOpenSubagents?.(row), children: [
2368
+ /* @__PURE__ */ jsx8(UiIcon, { name: "agents", size: 14 }),
2369
+ /* @__PURE__ */ jsx8("span", { children: row.subagentCount })
2370
+ ] }) : null,
2371
+ age ? /* @__PURE__ */ jsx8("time", { children: age }) : null
2372
+ ] }),
2373
+ unreadCount ? /* @__PURE__ */ jsx8("b", { "aria-label": `${unreadCount} unread messages`, children: unreadLabel }) : null
2373
2374
  ] }) : null
2374
2375
  ] });
2375
2376
  }
@@ -2259,31 +2259,32 @@ function SessionRow({ row, state, onOpen, onOpenSubagents, now = Date.now() }) {
2259
2259
  /* @__PURE__ */ jsxs6("button", { className: "scui-session-main", "data-session-key": row.key, type: "button", "aria-label": `${title} \xB7 ${harnessDisplayName(row.harness)}${row.writable ? "" : " \xB7 Read-only"}${working ? " \xB7 Working" : ""}${path.complete ? ` \xB7 ${path.complete}` : ""}${preview ? ` \xB7 ${preview}` : ""}${unreadCount ? ` \xB7 ${unreadCount} unread` : ""}${age ? ` \xB7 ${age}` : ""}`, "aria-current": row.active ? "true" : void 0, onClick: () => onOpen(row), children: [
2260
2260
  /* @__PURE__ */ jsx7(HarnessLogo, { id: row.harness, activity, size: 34 }),
2261
2261
  /* @__PURE__ */ jsxs6("span", { className: "scui-session-copy", children: [
2262
- /* @__PURE__ */ jsxs6("span", { className: "scui-session-title", children: [
2263
- /* @__PURE__ */ jsx7("strong", { children: title }),
2264
- /* @__PURE__ */ jsx7("span", { className: "scui-session-meta", children: age ? /* @__PURE__ */ jsx7("time", { children: age }) : null })
2265
- ] }),
2262
+ /* @__PURE__ */ jsx7("span", { className: "scui-session-title", children: /* @__PURE__ */ jsx7("strong", { children: title }) }),
2266
2263
  path.complete ? /* @__PURE__ */ jsxs6("small", { className: "scui-session-path", title: row.cwd, children: [
2267
2264
  /* @__PURE__ */ jsx7("span", { className: "scui-session-path-leading", children: path.leading }),
2268
2265
  path.separator ? /* @__PURE__ */ jsx7("span", { className: "scui-session-path-separator", children: path.separator }) : null,
2269
2266
  path.trailing ? /* @__PURE__ */ jsx7("span", { className: "scui-session-path-trailing", children: path.trailing }) : null
2270
2267
  ] }) : null,
2271
- working || preview || unreadCount ? /* @__PURE__ */ jsxs6("span", { className: "scui-session-preview", children: [
2268
+ working || preview ? /* @__PURE__ */ jsxs6("span", { className: "scui-session-preview", children: [
2272
2269
  working ? /* @__PURE__ */ jsxs6("span", { className: "scui-session-working", "aria-hidden": "true", children: [
2273
2270
  /* @__PURE__ */ jsx7("i", {}),
2274
2271
  /* @__PURE__ */ jsx7("i", {}),
2275
2272
  /* @__PURE__ */ jsx7("i", {})
2276
2273
  ] }) : null,
2277
- preview || working ? /* @__PURE__ */ jsx7("small", { children: preview || "Working\u2026" }) : null,
2278
- unreadCount ? /* @__PURE__ */ jsx7("b", { "aria-label": `${unreadCount} unread messages`, children: unreadLabel }) : null
2274
+ preview || working ? /* @__PURE__ */ jsx7("small", { children: preview || "Working\u2026" }) : null
2279
2275
  ] }) : null,
2280
2276
  state.attachError?.key === row.key ? /* @__PURE__ */ jsx7("em", { children: state.attachError.message }) : null
2281
2277
  ] })
2282
2278
  ] }),
2283
- row.subagentCount ? /* @__PURE__ */ jsxs6("button", { className: "scui-session-agents", type: "button", "aria-label": `Open ${row.subagentCount} agent sessions in ${title}`, onClick: () => onOpenSubagents?.(row), children: [
2284
- /* @__PURE__ */ jsx7(UiIcon, { name: "agents", size: 14 }),
2285
- "Agents \xB7 ",
2286
- row.subagentCount
2279
+ row.subagentCount || age || unreadCount ? /* @__PURE__ */ jsxs6("span", { className: "scui-session-rail", children: [
2280
+ /* @__PURE__ */ jsxs6("span", { className: "scui-session-rail-meta", children: [
2281
+ row.subagentCount ? /* @__PURE__ */ jsxs6("button", { className: "scui-session-agents", type: "button", title: `${row.subagentCount} agent sessions`, "aria-label": `Open ${row.subagentCount} agent sessions in ${title}`, onClick: () => onOpenSubagents?.(row), children: [
2282
+ /* @__PURE__ */ jsx7(UiIcon, { name: "agents", size: 14 }),
2283
+ /* @__PURE__ */ jsx7("span", { children: row.subagentCount })
2284
+ ] }) : null,
2285
+ age ? /* @__PURE__ */ jsx7("time", { children: age }) : null
2286
+ ] }),
2287
+ unreadCount ? /* @__PURE__ */ jsx7("b", { "aria-label": `${unreadCount} unread messages`, children: unreadLabel }) : null
2287
2288
  ] }) : null
2288
2289
  ] });
2289
2290
  }
@@ -351,31 +351,32 @@ function SessionRow({ row, state, onOpen, onOpenSubagents, now = Date.now() }) {
351
351
  /* @__PURE__ */ jsxs5("button", { className: "scui-session-main", "data-session-key": row.key, type: "button", "aria-label": `${title} \xB7 ${harnessDisplayName(row.harness)}${row.writable ? "" : " \xB7 Read-only"}${working ? " \xB7 Working" : ""}${path.complete ? ` \xB7 ${path.complete}` : ""}${preview ? ` \xB7 ${preview}` : ""}${unreadCount ? ` \xB7 ${unreadCount} unread` : ""}${age ? ` \xB7 ${age}` : ""}`, "aria-current": row.active ? "true" : void 0, onClick: () => onOpen(row), children: [
352
352
  /* @__PURE__ */ jsx6(HarnessLogo, { id: row.harness, activity, size: 34 }),
353
353
  /* @__PURE__ */ jsxs5("span", { className: "scui-session-copy", children: [
354
- /* @__PURE__ */ jsxs5("span", { className: "scui-session-title", children: [
355
- /* @__PURE__ */ jsx6("strong", { children: title }),
356
- /* @__PURE__ */ jsx6("span", { className: "scui-session-meta", children: age ? /* @__PURE__ */ jsx6("time", { children: age }) : null })
357
- ] }),
354
+ /* @__PURE__ */ jsx6("span", { className: "scui-session-title", children: /* @__PURE__ */ jsx6("strong", { children: title }) }),
358
355
  path.complete ? /* @__PURE__ */ jsxs5("small", { className: "scui-session-path", title: row.cwd, children: [
359
356
  /* @__PURE__ */ jsx6("span", { className: "scui-session-path-leading", children: path.leading }),
360
357
  path.separator ? /* @__PURE__ */ jsx6("span", { className: "scui-session-path-separator", children: path.separator }) : null,
361
358
  path.trailing ? /* @__PURE__ */ jsx6("span", { className: "scui-session-path-trailing", children: path.trailing }) : null
362
359
  ] }) : null,
363
- working || preview || unreadCount ? /* @__PURE__ */ jsxs5("span", { className: "scui-session-preview", children: [
360
+ working || preview ? /* @__PURE__ */ jsxs5("span", { className: "scui-session-preview", children: [
364
361
  working ? /* @__PURE__ */ jsxs5("span", { className: "scui-session-working", "aria-hidden": "true", children: [
365
362
  /* @__PURE__ */ jsx6("i", {}),
366
363
  /* @__PURE__ */ jsx6("i", {}),
367
364
  /* @__PURE__ */ jsx6("i", {})
368
365
  ] }) : null,
369
- preview || working ? /* @__PURE__ */ jsx6("small", { children: preview || "Working\u2026" }) : null,
370
- unreadCount ? /* @__PURE__ */ jsx6("b", { "aria-label": `${unreadCount} unread messages`, children: unreadLabel }) : null
366
+ preview || working ? /* @__PURE__ */ jsx6("small", { children: preview || "Working\u2026" }) : null
371
367
  ] }) : null,
372
368
  state.attachError?.key === row.key ? /* @__PURE__ */ jsx6("em", { children: state.attachError.message }) : null
373
369
  ] })
374
370
  ] }),
375
- row.subagentCount ? /* @__PURE__ */ jsxs5("button", { className: "scui-session-agents", type: "button", "aria-label": `Open ${row.subagentCount} agent sessions in ${title}`, onClick: () => onOpenSubagents?.(row), children: [
376
- /* @__PURE__ */ jsx6(UiIcon, { name: "agents", size: 14 }),
377
- "Agents \xB7 ",
378
- row.subagentCount
371
+ row.subagentCount || age || unreadCount ? /* @__PURE__ */ jsxs5("span", { className: "scui-session-rail", children: [
372
+ /* @__PURE__ */ jsxs5("span", { className: "scui-session-rail-meta", children: [
373
+ row.subagentCount ? /* @__PURE__ */ jsxs5("button", { className: "scui-session-agents", type: "button", title: `${row.subagentCount} agent sessions`, "aria-label": `Open ${row.subagentCount} agent sessions in ${title}`, onClick: () => onOpenSubagents?.(row), children: [
374
+ /* @__PURE__ */ jsx6(UiIcon, { name: "agents", size: 14 }),
375
+ /* @__PURE__ */ jsx6("span", { children: row.subagentCount })
376
+ ] }) : null,
377
+ age ? /* @__PURE__ */ jsx6("time", { children: age }) : null
378
+ ] }),
379
+ unreadCount ? /* @__PURE__ */ jsx6("b", { "aria-label": `${unreadCount} unread messages`, children: unreadLabel }) : null
379
380
  ] }) : null
380
381
  ] });
381
382
  }
package/sessions.mjs CHANGED
@@ -351,31 +351,32 @@ function SessionRow({ row, state, onOpen, onOpenSubagents, now = Date.now() }) {
351
351
  /* @__PURE__ */ jsxs5("button", { className: "scui-session-main", "data-session-key": row.key, type: "button", "aria-label": `${title} \xB7 ${harnessDisplayName(row.harness)}${row.writable ? "" : " \xB7 Read-only"}${working ? " \xB7 Working" : ""}${path.complete ? ` \xB7 ${path.complete}` : ""}${preview ? ` \xB7 ${preview}` : ""}${unreadCount ? ` \xB7 ${unreadCount} unread` : ""}${age ? ` \xB7 ${age}` : ""}`, "aria-current": row.active ? "true" : void 0, onClick: () => onOpen(row), children: [
352
352
  /* @__PURE__ */ jsx6(HarnessLogo, { id: row.harness, activity, size: 34 }),
353
353
  /* @__PURE__ */ jsxs5("span", { className: "scui-session-copy", children: [
354
- /* @__PURE__ */ jsxs5("span", { className: "scui-session-title", children: [
355
- /* @__PURE__ */ jsx6("strong", { children: title }),
356
- /* @__PURE__ */ jsx6("span", { className: "scui-session-meta", children: age ? /* @__PURE__ */ jsx6("time", { children: age }) : null })
357
- ] }),
354
+ /* @__PURE__ */ jsx6("span", { className: "scui-session-title", children: /* @__PURE__ */ jsx6("strong", { children: title }) }),
358
355
  path.complete ? /* @__PURE__ */ jsxs5("small", { className: "scui-session-path", title: row.cwd, children: [
359
356
  /* @__PURE__ */ jsx6("span", { className: "scui-session-path-leading", children: path.leading }),
360
357
  path.separator ? /* @__PURE__ */ jsx6("span", { className: "scui-session-path-separator", children: path.separator }) : null,
361
358
  path.trailing ? /* @__PURE__ */ jsx6("span", { className: "scui-session-path-trailing", children: path.trailing }) : null
362
359
  ] }) : null,
363
- working || preview || unreadCount ? /* @__PURE__ */ jsxs5("span", { className: "scui-session-preview", children: [
360
+ working || preview ? /* @__PURE__ */ jsxs5("span", { className: "scui-session-preview", children: [
364
361
  working ? /* @__PURE__ */ jsxs5("span", { className: "scui-session-working", "aria-hidden": "true", children: [
365
362
  /* @__PURE__ */ jsx6("i", {}),
366
363
  /* @__PURE__ */ jsx6("i", {}),
367
364
  /* @__PURE__ */ jsx6("i", {})
368
365
  ] }) : null,
369
- preview || working ? /* @__PURE__ */ jsx6("small", { children: preview || "Working\u2026" }) : null,
370
- unreadCount ? /* @__PURE__ */ jsx6("b", { "aria-label": `${unreadCount} unread messages`, children: unreadLabel }) : null
366
+ preview || working ? /* @__PURE__ */ jsx6("small", { children: preview || "Working\u2026" }) : null
371
367
  ] }) : null,
372
368
  state.attachError?.key === row.key ? /* @__PURE__ */ jsx6("em", { children: state.attachError.message }) : null
373
369
  ] })
374
370
  ] }),
375
- row.subagentCount ? /* @__PURE__ */ jsxs5("button", { className: "scui-session-agents", type: "button", "aria-label": `Open ${row.subagentCount} agent sessions in ${title}`, onClick: () => onOpenSubagents?.(row), children: [
376
- /* @__PURE__ */ jsx6(UiIcon, { name: "agents", size: 14 }),
377
- "Agents \xB7 ",
378
- row.subagentCount
371
+ row.subagentCount || age || unreadCount ? /* @__PURE__ */ jsxs5("span", { className: "scui-session-rail", children: [
372
+ /* @__PURE__ */ jsxs5("span", { className: "scui-session-rail-meta", children: [
373
+ row.subagentCount ? /* @__PURE__ */ jsxs5("button", { className: "scui-session-agents", type: "button", title: `${row.subagentCount} agent sessions`, "aria-label": `Open ${row.subagentCount} agent sessions in ${title}`, onClick: () => onOpenSubagents?.(row), children: [
374
+ /* @__PURE__ */ jsx6(UiIcon, { name: "agents", size: 14 }),
375
+ /* @__PURE__ */ jsx6("span", { children: row.subagentCount })
376
+ ] }) : null,
377
+ age ? /* @__PURE__ */ jsx6("time", { children: age }) : null
378
+ ] }),
379
+ unreadCount ? /* @__PURE__ */ jsx6("b", { "aria-label": `${unreadCount} unread messages`, children: unreadLabel }) : null
379
380
  ] }) : null
380
381
  ] });
381
382
  }
package/styles.css CHANGED
@@ -102,18 +102,18 @@
102
102
  .scui-search { display:flex; flex:1; min-width:0; align-items:center; gap:7px; padding:6px 8px; border:1px solid var(--scui-border); border-radius:8px; background:var(--scui-bg-raised); color:var(--scui-muted) }
103
103
  .scui-search input { flex:1; min-width:0; border:0; outline:0; background:transparent; color:var(--scui-fg) }
104
104
  .scui-session-rows { flex:1; min-height:0; overflow:auto; padding:0 6px 8px }
105
- .scui-session { display:flex; width:100%; align-items:center; border-radius:7px; background:transparent }
105
+ .scui-session { display:grid; grid-template-columns:minmax(0,1fr) auto; width:100%; align-items:stretch; border-radius:7px; background:transparent }
106
106
  .scui-session:hover,.scui-session[data-active="true"] { background:var(--scui-fill) }
107
- .scui-session-main { display:flex; min-width:0; flex:1; align-items:flex-start; gap:9px; padding:8px; border:0; background:transparent; text-align:left; cursor:pointer }
108
- .scui-session-agents { display:flex; flex:none; align-items:center; gap:4px; margin-right:6px; padding:4px 6px; border:0; border-radius:6px; background:transparent; color:var(--scui-muted); cursor:pointer; font-size:9px; white-space:nowrap }.scui-session-agents:hover { background:var(--scui-bg-raised); color:var(--scui-fg) }
107
+ .scui-session-main { display:flex; min-width:0; align-items:flex-start; gap:9px; padding:8px; border:0; background:transparent; text-align:left; cursor:pointer }
108
+ .scui-session-rail { display:grid; min-width:26px; align-content:space-between; justify-items:end; gap:4px; padding:8px 8px 8px 0 }.scui-session-rail-meta { display:flex; align-items:center; justify-content:flex-end; gap:4px }.scui-session-rail time { color:var(--scui-muted); font-size:10px; white-space:nowrap }.scui-session-rail > b { display:grid; min-width:16px; height:16px; padding:0 4px; place-items:center; border-radius:8px; background:var(--scui-accent); color:var(--scui-bg); font-size:9px; line-height:16px }
109
+ .scui-session-agents { display:flex; flex:none; align-items:center; gap:2px; padding:2px 4px; border:0; border-radius:6px; background:transparent; color:var(--scui-muted); cursor:pointer; font-size:9px; font-variant-numeric:tabular-nums; white-space:nowrap }.scui-session-agents:hover { background:var(--scui-bg-raised); color:var(--scui-fg) }
109
110
  .scui-session-copy { display:grid; flex:1; min-width:0; gap:1px }
110
111
  .scui-session-copy > span { display:flex; align-items:baseline; gap:7px; min-width:0 }
111
112
  .scui-session-copy strong,.scui-session-copy span > small { overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
112
- .scui-session-copy strong { flex:1; font-size:12.5px }.scui-session-meta { display:flex; flex:none; align-items:center }.scui-session-title time { color:var(--scui-muted); font-size:10px }.scui-session-working { display:flex; flex:none; width:14px; height:8px; align-self:center; align-items:center; justify-content:space-between; color:var(--scui-muted) }.scui-session-working > i { width:3px; height:3px; border-radius:50%; background:currentColor; animation:scui-session-dots 1.2s ease-in-out infinite }.scui-session-working > i:nth-child(2) { animation-delay:.15s }.scui-session-working > i:nth-child(3) { animation-delay:.3s }
113
+ .scui-session-copy strong { flex:1; font-size:12.5px }.scui-session-working { display:flex; flex:none; width:14px; height:8px; align-self:center; align-items:center; justify-content:space-between; color:var(--scui-muted) }.scui-session-working > i { width:3px; height:3px; border-radius:50%; background:currentColor; animation:scui-session-dots 1.2s ease-in-out infinite }.scui-session-working > i:nth-child(2) { animation-delay:.15s }.scui-session-working > i:nth-child(3) { animation-delay:.3s }
113
114
  .scui-session-path { display:flex; min-width:0; overflow:hidden; color:var(--scui-muted); font-size:9.5px; white-space:nowrap }
114
115
  .scui-session-path-leading { flex:0 1 auto; overflow:hidden; text-overflow:ellipsis }.scui-session-path-separator { flex:none }.scui-session-path-trailing { flex:none; max-width:58%; overflow:hidden; direction:rtl; text-align:left; text-overflow:ellipsis; unicode-bidi:plaintext }
115
- .scui-session-preview small { flex:1; min-width:0; color:var(--scui-muted); font-size:10.5px }.scui-session-preview b { display:grid; flex:none; min-width:16px; height:16px; padding:0 4px; place-items:center; border-radius:8px; background:var(--scui-accent); color:var(--scui-bg); font-size:9px; line-height:16px }
116
- .scui-session[data-writable="false"] .scui-session-preview b { background:var(--scui-muted) }
116
+ .scui-session-preview small { flex:1; min-width:0; color:var(--scui-muted); font-size:10.5px }.scui-session[data-writable="false"] .scui-session-rail > b { background:var(--scui-muted) }
117
117
  .scui-session-copy em { overflow:hidden; color:var(--scui-danger); font-size:10px; font-style:normal; text-overflow:ellipsis; white-space:nowrap }
118
118
  .scui-load { display:block; margin:7px auto; padding:5px 10px; border:1px solid var(--scui-border); border-radius:8px; background:var(--scui-bg-raised); color:var(--scui-muted); cursor:pointer }.scui-load:disabled { opacity:.65; cursor:default }
119
119