@volter-ai-dev/supercode-ui 0.1.1 → 0.1.3

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/composer.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export type { MessengerLabels, SupercodeUiState, UiAdapter } from './index.js';
2
+ export { Composer, ContinuationBar } from './index.js';
package/composer.mjs CHANGED
@@ -1,11 +1,12 @@
1
- // src/components.jsx
2
- import MarkdownIt from "markdown-it";
3
- import { useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks";
1
+ // src/composer.jsx
2
+ import { useEffect, useRef, useState } from "preact/hooks";
4
3
 
5
4
  // core.mjs
6
5
  var HARNESS_NAMES = Object.freeze({
7
6
  "claude-code": "Claude Code",
8
7
  codex: "Codex",
8
+ gemini: "Gemini CLI",
9
+ goose: "Goose",
9
10
  opencode: "OpenCode",
10
11
  pi: "Pi",
11
12
  grok: "Grok"
@@ -70,15 +71,16 @@ function isSendKey(event) {
70
71
  return event.key === "Enter" && !event.shiftKey && !event.isComposing;
71
72
  }
72
73
 
73
- // src/components.jsx
74
+ // src/memory.js
75
+ var MEMORY_LIMIT = 100;
76
+ function boundedSet(map, key, value) {
77
+ map.delete(key);
78
+ map.set(key, value);
79
+ while (map.size > MEMORY_LIMIT) map.delete(map.keys().next().value);
80
+ }
81
+
82
+ // src/composer.jsx
74
83
  import { jsx, jsxs } from "preact/jsx-runtime";
75
- var markdown = new MarkdownIt({ html: false, linkify: true, breaks: false });
76
- var defaultLinkOpen = markdown.renderer.rules.link_open;
77
- markdown.renderer.rules.link_open = (tokens, index, options, env, self) => {
78
- tokens[index]?.attrSet("target", "_blank");
79
- tokens[index]?.attrSet("rel", "noreferrer noopener");
80
- return defaultLinkOpen ? defaultLinkOpen(tokens, index, options, env, self) : self.renderToken(tokens, index, options);
81
- };
82
84
  var composerMemory = /* @__PURE__ */ new Map();
83
85
  function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
84
86
  if (state.mode !== "mirror" || state.canSend) return null;
@@ -105,11 +107,17 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
105
107
  const [draft, setDraft] = useState(remembered.draft);
106
108
  const [queue, setQueue] = useState(remembered.queue);
107
109
  const textarea = useRef(null);
110
+ const remember = (nextDraft, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, queue: nextQueue });
111
+ const updateQueue = (update) => setQueue((items) => {
112
+ const next = update(items);
113
+ remember(draft, next);
114
+ return next;
115
+ });
108
116
  useEffect(() => {
109
117
  if (!state.busy && state.canSend && queue.length) {
110
118
  const [next, ...rest] = queue;
111
119
  setQueue(rest);
112
- composerMemory.set(memoryKey, { draft, queue: rest });
120
+ remember(draft, rest);
113
121
  onPending?.(next);
114
122
  adapter.onIntent({ action: "send", text: next });
115
123
  }
@@ -121,17 +129,13 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
121
129
  const send = () => {
122
130
  const text = draft.trim();
123
131
  if (!text) return;
124
- if (state.busy) setQueue((items) => {
125
- const next = [...items, text];
126
- composerMemory.set(memoryKey, { draft: "", queue: next });
127
- return next;
128
- });
132
+ if (state.busy) updateQueue((items) => [...items, text]);
129
133
  else if (state.canSend) {
130
134
  onPending?.(text);
131
135
  adapter.onIntent({ action: "send", text });
132
136
  } else return;
133
137
  setDraft("");
134
- composerMemory.set(memoryKey, { draft: "", queue: state.busy ? [...queue, text] : queue });
138
+ remember("", state.busy ? [...queue, text] : queue);
135
139
  };
136
140
  return /* @__PURE__ */ jsxs("div", { class: "scui-compose", children: [
137
141
  queue.length ? /* @__PURE__ */ jsxs("div", { class: "scui-queue", children: [
@@ -141,14 +145,14 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
141
145
  ] }),
142
146
  queue.map((item, index) => /* @__PURE__ */ jsxs("span", { children: [
143
147
  item,
144
- /* @__PURE__ */ jsx("button", { "aria-label": `Remove queued message ${index + 1}`, onClick: () => setQueue((items) => items.filter((_, itemIndex) => itemIndex !== index)), children: "\xD7" })
148
+ /* @__PURE__ */ jsx("button", { type: "button", "aria-label": `Remove queued message ${index + 1}`, onClick: () => updateQueue((items) => items.filter((_, itemIndex) => itemIndex !== index)), children: "\xD7" })
145
149
  ] }, `${index}:${item}`))
146
150
  ] }) : null,
147
151
  /* @__PURE__ */ jsxs("div", { class: "scui-envelope", children: [
148
152
  /* @__PURE__ */ jsx("textarea", { ref: textarea, rows: 1, "aria-label": `Message ${harnessDisplayName(state.harness) || "agent"}`, placeholder: state.startup !== "ready" ? "Connecting\u2026" : state.busy ? "Queue a follow-up\u2026" : labels.askAgent, value: draft, disabled: state.mode !== "control" && !state.canSend, onInput: (event) => {
149
153
  const value = event.currentTarget.value;
150
154
  setDraft(value);
151
- composerMemory.set(memoryKey, { draft: value, queue });
155
+ remember(value, queue);
152
156
  }, onKeyDown: (event) => {
153
157
  if (isSendKey(event)) {
154
158
  event.preventDefault();
package/controller.d.ts CHANGED
@@ -7,7 +7,10 @@ import type {
7
7
  SupercodeController,
8
8
  } from '@volter-ai-dev/supercode-client';
9
9
  import type {
10
+ AttachedSessionModel,
10
11
  SessionAttention,
12
+ SessionRowModel,
13
+ StartupPhase,
11
14
  SupercodeUiIntent,
12
15
  SupercodeUiState,
13
16
  UiAdapter,
@@ -15,12 +18,33 @@ import type {
15
18
 
16
19
  export interface ClientProjectionOptions {
17
20
  now?: number;
21
+ /** Maximum rendered conversation rows after hidden context is removed. Default 120. */
22
+ maxEntries?: number;
23
+ /** Maximum native entries inspected to fill the rendered tail. Default four times `maxEntries`. */
24
+ maxScanEntries?: number;
25
+ /** Maximum characters retained independently in each rendered entry field. Default 16,000. */
26
+ maxEntryChars?: number;
27
+ /** Maximum controller-inventory rows projected when the host does not supply `sessions`. Default 100. */
28
+ maxSessions?: number;
29
+ /** Age in milliseconds for treating an inventory row as recently live. Default five minutes. */
30
+ liveWindowMs?: number;
31
+ /** Bounded native fidelity details; total counts remain truthful when arrays are shortened. */
32
+ maxResidueItems?: number;
33
+ maxResidueChars?: number;
34
+ maxSubagents?: number;
35
+ /** Host bootstrap phase before the controller itself is ready. */
36
+ startup?: StartupPhase;
18
37
  attention?: SessionAttention[];
19
38
  savedDraft?: string;
20
39
  history?: Partial<SupercodeUiState['history']>;
21
40
  exportBackTarget?: SessionFormat | null;
22
41
  exportReceipt?: SupercodeUiState['exportReceipt'];
23
42
  reductionReceipt?: SupercodeUiState['reductionReceipt'];
43
+ /** Trusted-host inventory and lifecycle overlays (for example a machine-wide session catalog). */
44
+ sessions?: SessionRowModel[];
45
+ attached?: AttachedSessionModel | null;
46
+ owned?: AttachedSessionModel | null;
47
+ attachError?: SupercodeUiState['attachError'];
24
48
  }
25
49
 
26
50
  export function projectClientSnapshot(
package/controller.mjs CHANGED
@@ -3,11 +3,22 @@ import { normalizeUiState } from './core.mjs';
3
3
  const HARNESS_LABELS = {
4
4
  'claude-code': 'Claude Code',
5
5
  codex: 'Codex',
6
+ gemini: 'Gemini CLI',
7
+ goose: 'Goose',
6
8
  opencode: 'OpenCode',
7
9
  pi: 'Pi',
8
10
  grok: 'Grok',
9
11
  };
10
12
 
13
+ const DEFAULT_MAX_ENTRIES = 120;
14
+ const DEFAULT_MAX_ENTRY_CHARS = 16_000;
15
+ const DEFAULT_MAX_SESSIONS = 100;
16
+ const DEFAULT_LIVE_WINDOW_MS = 300_000;
17
+ const MAX_PILL_LABEL_CHARS = 72;
18
+ const DEFAULT_MAX_RESIDUE_ITEMS = 20;
19
+ const DEFAULT_MAX_RESIDUE_CHARS = 300;
20
+ const DEFAULT_MAX_SUBAGENTS = 50;
21
+
11
22
  function label(id, supplied) {
12
23
  return supplied || HARNESS_LABELS[id] || id;
13
24
  }
@@ -36,56 +47,145 @@ function payloadText(payload) {
36
47
  }
37
48
  }
38
49
 
39
- function projectConversation(conversation) {
40
- return conversation.flatMap((entry) => {
41
- if (!entry || typeof entry !== 'object' || typeof entry.id !== 'string') return [];
42
- if (entry.kind === 'message') {
43
- return [{
44
- id: entry.id,
45
- role: entry.role,
46
- text: entry.text ?? '',
47
- ts: null,
48
- truncated: false,
49
- ...(Array.isArray(entry.context) ? { context: entry.context } : {}),
50
- }];
50
+ function cloneJsonValue(value) {
51
+ if (Array.isArray(value)) return value.map(cloneJsonValue);
52
+ if (value !== null && typeof value === 'object') {
53
+ return Object.fromEntries(Object.entries(value).map(([key, nested]) => [key, cloneJsonValue(nested)]));
54
+ }
55
+ return value;
56
+ }
57
+
58
+ function positiveInteger(value, fallback) {
59
+ return Number.isInteger(value) && value > 0 ? value : fallback;
60
+ }
61
+
62
+ function truncate(value, max) {
63
+ const text = typeof value === 'string' ? value : '';
64
+ if (text.length <= max) return { text, truncated: false };
65
+ let end = max;
66
+ const last = text.charCodeAt(end - 1);
67
+ const next = text.charCodeAt(end);
68
+ if (last >= 0xd800 && last <= 0xdbff && next >= 0xdc00 && next <= 0xdfff) end -= 1;
69
+ return { text: `${text.slice(0, end)}…`, truncated: true };
70
+ }
71
+
72
+ function timestampFromMetadata(metadata) {
73
+ if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) return null;
74
+ for (const key of ['timestamp', 'ts', 'time', 'created_at', 'createdAt', 'date']) {
75
+ const raw = metadata[key];
76
+ if (typeof raw !== 'string' || raw === '') continue;
77
+ if (/^\d+$/.test(raw)) {
78
+ const value = Number(raw);
79
+ if (Number.isFinite(value) && value > 0) return value < 1e11 ? value * 1000 : value;
80
+ continue;
51
81
  }
52
- if (entry.kind === 'tool') {
53
- return [{
54
- id: entry.id,
55
- role: 'tool',
56
- text: entry.resultText ?? '',
57
- ts: null,
58
- truncated: false,
59
- ...(entry.name ? { label: entry.name } : {}),
60
- ...(entry.arguments ? { arguments: entry.arguments } : {}),
61
- resultText: entry.resultText ?? '',
82
+ const value = Date.parse(raw);
83
+ if (Number.isFinite(value)) return value;
84
+ }
85
+ return null;
86
+ }
87
+
88
+ function projectContext(context) {
89
+ if (!Array.isArray(context)) return undefined;
90
+ const projected = context.flatMap((item) => item && typeof item === 'object'
91
+ && typeof item.label === 'string' && typeof item.detail === 'string'
92
+ ? [{
93
+ ...(typeof item.id === 'string' ? { id: item.id } : {}),
94
+ ...(typeof item.kind === 'string' ? { kind: item.kind } : {}),
95
+ label: item.label,
96
+ detail: item.detail,
97
+ }]
98
+ : []);
99
+ return projected.length ? projected : undefined;
100
+ }
101
+
102
+ function requestSummary(entry) {
103
+ const options = Array.isArray(entry.options)
104
+ ? entry.options.map((option) => option?.name).filter(Boolean).join(' / ')
105
+ : '';
106
+ const resolution = entry.resolution?.name ? ` → ${entry.resolution.name}` : '';
107
+ return options ? `${entry.requestKind}: ${options}${resolution}` : `${entry.requestKind ?? 'request'}${resolution}`;
108
+ }
109
+
110
+ function projectConversationEntry(entry, maxEntryChars) {
111
+ if (!entry || typeof entry !== 'object' || typeof entry.id !== 'string') return null;
112
+ if (entry.kind === 'message') {
113
+ if (entry.visibility === 'context') return null;
114
+ const body = truncate(entry.text, maxEntryChars);
115
+ const context = projectContext(entry.context);
116
+ return {
117
+ id: entry.id,
118
+ role: entry.role,
119
+ text: body.text,
120
+ ts: timestampFromMetadata(entry.metadata),
121
+ truncated: body.truncated,
122
+ ...(context ? { context } : {}),
123
+ };
124
+ }
125
+ if (entry.kind === 'tool') {
126
+ const argumentsText = truncate((entry.arguments ?? '').trim(), maxEntryChars);
127
+ const result = truncate((entry.resultText ?? '').trim(), maxEntryChars);
128
+ const primary = entry.status === 'pending' ? argumentsText : result;
129
+ return {
130
+ id: entry.id,
131
+ role: 'tool',
132
+ text: primary.text,
133
+ ts: timestampFromMetadata(entry.metadata),
134
+ truncated: argumentsText.truncated || result.truncated,
135
+ label: entry.name ?? 'tool',
136
+ arguments: argumentsText.text,
137
+ resultText: result.text,
138
+ status: entry.status,
139
+ };
140
+ }
141
+ if (entry.kind === 'reasoning') {
142
+ const body = truncate(entry.text, maxEntryChars);
143
+ return { id: entry.id, role: 'reasoning', text: body.text, ts: null, truncated: body.truncated, streaming: entry.streaming === true };
144
+ }
145
+ if (entry.kind === 'request') {
146
+ const summary = truncate(requestSummary(entry), maxEntryChars);
147
+ const payload = truncate(payloadText(entry.payload), maxEntryChars);
148
+ return {
149
+ id: entry.id,
150
+ role: 'request',
151
+ text: summary.text,
152
+ ts: null,
153
+ truncated: summary.truncated || payload.truncated,
154
+ request: {
155
+ requestId: cloneJsonValue(entry.requestId),
156
+ requestKind: entry.requestKind,
157
+ payloadText: payload.text,
158
+ options: Array.isArray(entry.options) ? entry.options.map((option) => ({ ...option })) : [],
159
+ cancellable: entry.cancellable === true,
62
160
  status: entry.status,
63
- }];
64
- }
65
- if (entry.kind === 'reasoning') {
66
- return [{ id: entry.id, role: 'reasoning', text: entry.text ?? '', ts: null, truncated: false, streaming: entry.streaming === true }];
67
- }
68
- if (entry.kind === 'request') {
69
- return [{
70
- id: entry.id,
71
- role: 'request',
72
- text: payloadText(entry.payload),
73
- ts: null,
74
- truncated: false,
75
- request: {
76
- requestId: entry.requestId,
77
- requestKind: entry.requestKind,
78
- payloadText: payloadText(entry.payload),
79
- options: entry.options ?? [],
80
- cancellable: entry.cancellable === true,
81
- status: entry.status,
82
- resolution: entry.resolution ?? null,
83
- },
84
- }];
85
- }
86
- if (entry.kind === 'notice') return [{ id: entry.id, role: 'notice', text: entry.text ?? '', ts: null, truncated: false, code: entry.code }];
87
- return [];
88
- });
161
+ resolution: entry.resolution ? { ...entry.resolution } : null,
162
+ },
163
+ };
164
+ }
165
+ if (entry.kind === 'notice') {
166
+ const body = truncate(entry.text || entry.code, maxEntryChars);
167
+ return { id: entry.id, role: 'notice', text: body.text, ts: null, truncated: body.truncated, code: entry.code };
168
+ }
169
+ return null;
170
+ }
171
+
172
+ function projectConversation(conversation, options) {
173
+ if (!Array.isArray(conversation)) return [];
174
+ const maxEntries = positiveInteger(options.maxEntries, DEFAULT_MAX_ENTRIES);
175
+ const maxEntryChars = positiveInteger(options.maxEntryChars, DEFAULT_MAX_ENTRY_CHARS);
176
+ const maxScanEntries = positiveInteger(options.maxScanEntries, maxEntries * 4);
177
+ const rows = [];
178
+ // Start at the tail and stop as soon as the bounded render window is full. Hidden harness
179
+ // context never consumes a slot. The independent scan cap prevents a corrupt/all-context
180
+ // transcript from turning this display projection back into an unbounded whole-history walk.
181
+ for (let index = conversation.length - 1, scanned = 0;
182
+ index >= 0 && rows.length < maxEntries && scanned < maxScanEntries;
183
+ index -= 1, scanned += 1) {
184
+ const row = projectConversationEntry(conversation[index], maxEntryChars);
185
+ if (row) rows.push(row);
186
+ }
187
+ rows.reverse();
188
+ return rows;
89
189
  }
90
190
 
91
191
  function attached(snapshot) {
@@ -106,16 +206,20 @@ function attached(snapshot) {
106
206
  };
107
207
  }
108
208
 
109
- function semantics(session) {
209
+ function semantics(session, options) {
110
210
  if (!session) return { fidelity: null, residue: [], residueCount: 0, parseErrors: 0, rawRecords: 0, subagents: [] };
211
+ const residue = Array.isArray(session.residue) ? session.residue : [];
212
+ const maxResidueItems = positiveInteger(options.maxResidueItems, DEFAULT_MAX_RESIDUE_ITEMS);
213
+ const maxResidueChars = positiveInteger(options.maxResidueChars, DEFAULT_MAX_RESIDUE_CHARS);
214
+ const maxSubagents = positiveInteger(options.maxSubagents, DEFAULT_MAX_SUBAGENTS);
111
215
  return {
112
216
  fidelity: session.fidelity ?? null,
113
- residue: Array.isArray(session.residue) ? session.residue : [],
114
- residueCount: Array.isArray(session.residue) ? session.residue.length : 0,
217
+ residue: residue.slice(0, maxResidueItems).map((item) => truncate(String(item), maxResidueChars).text),
218
+ residueCount: residue.length,
115
219
  parseErrors: Number.isFinite(session.parse_error_lines) ? session.parse_error_lines : 0,
116
220
  rawRecords: Number.isFinite(session.raw_record_count) ? session.raw_record_count : 0,
117
- subagents: Array.isArray(session.subagents) ? session.subagents.map((child, index) => ({
118
- id: child.session_id ?? `${child.source}:${index}`,
221
+ subagents: Array.isArray(session.subagents) ? session.subagents.slice(0, maxSubagents).map((child, index) => ({
222
+ id: child.session_id ?? child.agent_id ?? `${child.source}:${index}`,
119
223
  source: child.source,
120
224
  model: child.model ?? null,
121
225
  messages: Array.isArray(child.messages) ? child.messages.length : 0,
@@ -124,12 +228,33 @@ function semantics(session) {
124
228
  };
125
229
  }
126
230
 
231
+ function projectPill(snapshot) {
232
+ const text = (value) => truncate(value, MAX_PILL_LABEL_CHARS).text;
233
+ const harness = snapshot.activeHarness ?? 'agent';
234
+ if (snapshot.availability === 'loading') return { tone: 'off', label: text('connecting…') };
235
+ if (snapshot.availability === 'unavailable' || snapshot.availability === 'error') {
236
+ return { tone: 'dead', label: text(snapshot.error?.message ?? 'supercode unavailable') };
237
+ }
238
+ if (snapshot.error) return { tone: 'warn', label: text(snapshot.error.message) };
239
+ if (snapshot.turn?.state === 'running') return { tone: 'live', label: text(`${label(harness)} working…`) };
240
+ if (snapshot.turn?.state === 'interrupting') return { tone: 'warn', label: text('interrupting…') };
241
+ if (snapshot.turn?.state === 'reconciling') return { tone: 'live', label: text('syncing…') };
242
+ if (snapshot.connection?.mode === 'none') return { tone: 'off', label: text('no session') };
243
+ if (snapshot.connection?.mode === 'mirror') {
244
+ return snapshot.connection.messaging === 'live_peer'
245
+ ? { tone: 'live', label: text(`${label(harness)} live`) }
246
+ : { tone: 'warn', label: text(`${label(harness)} (read-only)`) };
247
+ }
248
+ return { tone: 'live', label: text(`${label(harness)} ready`) };
249
+ }
250
+
127
251
  export function projectClientSnapshot(snapshot, options = {}) {
128
252
  const now = Number.isFinite(options.now) ? options.now : Date.now();
129
- const busy = snapshot.turn?.state && snapshot.turn.state !== 'idle';
253
+ const busy = ['running', 'interrupting', 'reconciling'].includes(snapshot.turn?.state);
130
254
  const actions = snapshot.availableActions ?? {};
131
- const active = attached(snapshot);
132
- const sessions = (snapshot.sessions ?? []).map((session) => ({
255
+ const maxSessions = positiveInteger(options.maxSessions, DEFAULT_MAX_SESSIONS);
256
+ const liveWindowMs = positiveInteger(options.liveWindowMs, DEFAULT_LIVE_WINDOW_MS);
257
+ const snapshotSessions = (snapshot.sessions ?? []).slice(0, maxSessions).map((session) => ({
133
258
  key: session.key,
134
259
  harness: session.harness,
135
260
  name: workspaceName(session.cwd),
@@ -139,20 +264,23 @@ export function projectClientSnapshot(snapshot, options = {}) {
139
264
  updatedAt: session.updatedAt ?? null,
140
265
  messages: session.messageCount ?? null,
141
266
  active: session.key === snapshot.activeSessionKey,
142
- live: typeof session.updatedAt === 'number' && now - session.updatedAt <= 300_000,
267
+ live: typeof session.updatedAt === 'number' && now - session.updatedAt <= liveWindowMs,
143
268
  runtimeStatus: session.liveStatus ?? null,
144
269
  }));
270
+ const sessions = options.sessions ?? snapshotSessions;
271
+ const active = options.attached === undefined ? attached(snapshot) : options.attached;
272
+ const owned = options.owned === undefined
273
+ ? snapshot.connection?.ownsRuntime ? active : null
274
+ : options.owned;
275
+ const maxEntries = positiveInteger(options.maxEntries, DEFAULT_MAX_ENTRIES);
276
+ const transcript = projectConversation(snapshot.conversation ?? [], options);
145
277
  const startup = snapshot.availability === 'loading'
146
278
  ? snapshot.operation === 'start' ? 'starting' : snapshot.operation === 'refresh' ? 'discovering' : 'connecting'
147
279
  : 'ready';
148
280
  return normalizeUiState({
149
- pill: snapshot.error
150
- ? { tone: 'dead', label: snapshot.error.message }
151
- : busy ? { tone: 'live', label: `${label(snapshot.activeHarness)} working` }
152
- : snapshot.availability === 'ready' ? { tone: 'live', label: `${label(snapshot.activeHarness) || 'Supercode'} ready` }
153
- : { tone: 'off', label: 'connecting…' },
154
- startup,
155
- transcript: projectConversation(snapshot.conversation ?? []),
281
+ pill: projectPill(snapshot),
282
+ startup: options.startup ?? startup,
283
+ transcript,
156
284
  busy,
157
285
  operation: snapshot.operation,
158
286
  needsInput: Array.isArray(snapshot.requests) && snapshot.requests.some((request) => request.status === 'pending'),
@@ -177,7 +305,7 @@ export function projectClientSnapshot(snapshot, options = {}) {
177
305
  residueCount: snapshot.taskPlan?.residue?.length ?? 0,
178
306
  observedAt: snapshot.taskPlan?.observedAt ?? null,
179
307
  },
180
- semantics: semantics(snapshot.activeSession),
308
+ semantics: semantics(snapshot.activeSession, options),
181
309
  terminalHandoff: snapshot.terminalLaunch ? {
182
310
  program: snapshot.terminalLaunch.program,
183
311
  arguments: snapshot.terminalLaunch.arguments,
@@ -198,15 +326,15 @@ export function projectClientSnapshot(snapshot, options = {}) {
198
326
  history: {
199
327
  sessionLimit: options.history?.sessionLimit ?? sessions.length,
200
328
  hasMoreSessions: options.history?.hasMoreSessions ?? false,
201
- transcriptLimit: options.history?.transcriptLimit ?? Math.max(120, snapshot.conversation?.length ?? 0),
202
- hasEarlier: options.history?.hasEarlier ?? false,
329
+ transcriptLimit: options.history?.transcriptLimit ?? maxEntries,
330
+ hasEarlier: options.history?.hasEarlier ?? (snapshot.conversation?.length ?? 0) > maxEntries,
203
331
  },
204
332
  savedDraft: options.savedDraft ?? '',
205
333
  attention: options.attention ?? [],
206
334
  sessions,
207
335
  attached: active,
208
- owned: snapshot.connection?.ownsRuntime ? active : null,
209
- attachError: null,
336
+ owned,
337
+ attachError: options.attachError ?? null,
210
338
  });
211
339
  }
212
340
 
@@ -0,0 +1,20 @@
1
+ export type {
2
+ ActivityGroupProps,
3
+ MessengerComponents,
4
+ MessengerSlots,
5
+ SessionSemanticsModel,
6
+ SupercodeUiState,
7
+ TaskPlanModel,
8
+ TranscriptEntryModel,
9
+ TranscriptEntryProps,
10
+ UiAdapter,
11
+ } from './index.js';
12
+ export {
13
+ ActivityGroup,
14
+ Conversation,
15
+ LoadingStatus,
16
+ RequestCard,
17
+ SessionDetails,
18
+ TaskPlan,
19
+ TranscriptEntry,
20
+ } from './index.js';