@volter-ai-dev/supercode-ui 0.1.35 → 0.1.36

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
@@ -117,6 +117,9 @@ before counting visible rows, retains native timestamps and typed tool/request l
117
117
  the uncapped residue count truthful. Hosts can narrow those limits with `ClientProjectionOptions`
118
118
  and can overlay a machine-wide session inventory, pagination state, attention, drafts, attachment
119
119
  errors, and owned/attached identities without reimplementing transcript or capability semantics.
120
+ `projectSessionInventory` performs the corresponding title, latest-preview, activity, path, age,
121
+ sorting, and row projection for raw machine-wide descriptors; the trusted host supplies only its
122
+ opaque-key callback and retains the reversible locator map.
120
123
 
121
124
  ## Modularity contract
122
125
 
@@ -140,6 +143,9 @@ errors, and owned/attached identities without reimplementing transcript or capab
140
143
 
141
144
  `SupercodeUiState` is a transport-safe view model, not a duplicate controller. A trusted host maps
142
145
  `SupercodeController` snapshots and persisted inventory into it, then handles `SupercodeUiIntent`.
146
+ An iframe or extension host can pass unknown payloads through `parseSupercodeUiIntent` before using
147
+ `dispatchControllerIntent`; transport-specific operations can be intercepted with `handleIntent`
148
+ while ordinary controller semantics continue through the shared dispatcher.
143
149
  The browser cannot supply locators, credentials, policy, environment variables, or arbitrary
144
150
  materialization paths. Session keys and target harnesses must be revalidated by the host.
145
151
  Harness configuration is similarly narrow: the UI can only submit a choice from the revisioned
package/controller.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  SessionArtifact,
3
+ SessionDescriptor,
3
4
  SessionFormat,
4
5
  } from '@volter-ai-dev/supercode-harness-sdk';
5
6
  import type {
@@ -55,6 +56,30 @@ export function projectClientSnapshot(
55
56
  options?: ClientProjectionOptions,
56
57
  ): SupercodeUiState;
57
58
 
59
+ export interface SessionInventoryProjectionOptions {
60
+ /** Mint a transport-safe key while retaining the reversible locator only in the trusted host. */
61
+ keyFor(descriptor: SessionDescriptor): string;
62
+ now?: number;
63
+ home?: string;
64
+ active?: { harness: string; sessionId: string | null } | null;
65
+ maxSessions?: number;
66
+ liveWindowMs?: number;
67
+ preserveOrder?: boolean;
68
+ }
69
+
70
+ export function sessionConversationUpdatedAt(descriptor: SessionDescriptor): number | null;
71
+ export function sessionDescriptorRuntimeStatus(
72
+ descriptor: SessionDescriptor,
73
+ ): 'running' | 'busy' | 'idle' | null;
74
+ export interface ProjectedSessionRowModel extends SessionRowModel {
75
+ preview: string;
76
+ previewUpdatedAt: number | null;
77
+ }
78
+ export function projectSessionInventory(
79
+ descriptors: readonly SessionDescriptor[],
80
+ options: SessionInventoryProjectionOptions,
81
+ ): ProjectedSessionRowModel[];
82
+
58
83
  export interface ResolvableTranscriptImage {
59
84
  id?: string;
60
85
  label: string;
@@ -79,6 +104,8 @@ export interface ControllerBindingOptions {
79
104
  onIntent?: (intent: SupercodeUiIntent) => void | Promise<void>;
80
105
  onUnsupported?: (intent: SupercodeUiIntent) => void | Promise<void>;
81
106
  onError?: (error: unknown, intent: SupercodeUiIntent) => void | Promise<void>;
107
+ /** Return true after handling an intent to replace the standard controller mapping. */
108
+ handleIntent?: (intent: SupercodeUiIntent) => boolean | Promise<boolean>;
82
109
  onArtifact?: (artifact: SessionArtifact, intent: Extract<SupercodeUiIntent, { action: 'export' }>) => void | Promise<void>;
83
110
  onDraft?: (text: string) => void | Promise<void>;
84
111
  onAcknowledge?: (key: string) => void | Promise<void>;
@@ -90,6 +117,18 @@ export interface ControllerBindingOptions {
90
117
  resolveImage?: UiAdapter['resolveImage'];
91
118
  }
92
119
 
120
+ export interface ControllerIntentTarget {
121
+ getSnapshot(): SupercodeClientSnapshot;
122
+ dispatch(action: Parameters<SupercodeController['dispatch']>[0]): Promise<SupercodeClientSnapshot>;
123
+ exportSession?: SupercodeController['exportSession'];
124
+ }
125
+
126
+ export function dispatchControllerIntent(
127
+ controller: ControllerIntentTarget,
128
+ intent: SupercodeUiIntent,
129
+ options?: ControllerBindingOptions,
130
+ ): Promise<void>;
131
+
93
132
  export interface SupercodeUiBinding {
94
133
  adapter: UiAdapter;
95
134
  getState(): SupercodeUiState;
package/controller.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { conversationPreviewText } from '@volter-ai-dev/supercode-client';
1
2
  import { createToolPresentation, normalizeUiState, relativeAge } from './core.mjs';
2
3
 
3
4
  const HARNESS_LABELS = {
@@ -28,6 +29,110 @@ function workspaceName(cwd) {
28
29
  return cwd.replaceAll('\\', '/').split('/').filter(Boolean).at(-1) ?? cwd;
29
30
  }
30
31
 
32
+ function shortWorkspacePath(cwd, home) {
33
+ if (typeof cwd !== 'string' || !cwd) return '';
34
+ const root = typeof home === 'string' && home.endsWith('/') ? home.slice(0, -1) : home;
35
+ return root && (cwd === root || cwd.startsWith(`${root}/`)) ? `~${cwd.slice(root.length)}` : cwd;
36
+ }
37
+
38
+ function compactTopic(value) {
39
+ const text = typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '';
40
+ if (text.length <= 72) return text;
41
+ const prefix = text.slice(0, 71);
42
+ const boundary = prefix.lastIndexOf(' ');
43
+ return `${prefix.slice(0, boundary >= 40 ? boundary : 71).trimEnd()}…`;
44
+ }
45
+
46
+ export function sessionDescriptorRuntimeStatus(descriptor) {
47
+ const activity = descriptor?.activity;
48
+ if (activity && typeof activity === 'object') {
49
+ if (activity.presence === 'persisted') return null;
50
+ if (activity.turn === 'working') return 'busy';
51
+ if (activity.turn === 'idle' || activity.turn === 'needs_input') return 'idle';
52
+ if (activity.presence === 'running' || activity.presence === 'shutting_down') return 'running';
53
+ }
54
+ return ['running', 'busy', 'idle'].includes(descriptor?.live_status)
55
+ ? descriptor.live_status
56
+ : null;
57
+ }
58
+
59
+ function descriptorPreview(descriptor) {
60
+ for (const candidate of descriptor?.latest_message_candidates ?? []) {
61
+ const text = conversationPreviewText([candidate]);
62
+ if (!text) continue;
63
+ return { text, updatedAt: timestampFromMetadata(candidate.metadata) };
64
+ }
65
+ return null;
66
+ }
67
+
68
+ function descriptorTitle(descriptor) {
69
+ const nativeTitle = typeof descriptor?.title === 'string' ? descriptor.title.trim() : '';
70
+ const workspace = workspaceName(descriptor?.cwd);
71
+ if (nativeTitle && nativeTitle !== workspace && nativeTitle !== descriptor?.cwd) {
72
+ return compactTopic(nativeTitle);
73
+ }
74
+ if (descriptor?.locator?.harness !== 'claude-code' && descriptor?.locator?.harness !== 'codex') {
75
+ return compactTopic(nativeTitle || workspace || descriptor?.locator?.session_id?.slice(0, 8)) || 'Untitled chat';
76
+ }
77
+ const openingUserMessages = (descriptor?.preview_candidates ?? []).filter(
78
+ (candidate) => candidate?.role === undefined || candidate.role === 'user',
79
+ );
80
+ return compactTopic(conversationPreviewText(openingUserMessages)) || 'Untitled chat';
81
+ }
82
+
83
+ /** Timestamp represented by a messenger row, independent of native store heartbeat writes. */
84
+ export function sessionConversationUpdatedAt(descriptor) {
85
+ return descriptorPreview(descriptor)?.updatedAt ?? descriptor?.updated_at_ms ?? null;
86
+ }
87
+
88
+ /**
89
+ * Project trusted machine-wide descriptors into the same bounded rows as controller snapshots.
90
+ * The host supplies opaque keys because only it may retain the reversible locator mapping.
91
+ */
92
+ export function projectSessionInventory(descriptors, options) {
93
+ if (typeof options?.keyFor !== 'function') {
94
+ throw new TypeError('projectSessionInventory requires an opaque keyFor callback');
95
+ }
96
+ const now = Number.isFinite(options.now) ? options.now : Date.now();
97
+ const maxSessions = positiveInteger(options.maxSessions, DEFAULT_MAX_SESSIONS);
98
+ const liveWindowMs = positiveInteger(options.liveWindowMs, DEFAULT_LIVE_WINDOW_MS);
99
+ const active = options.active;
100
+ const seen = new Set();
101
+ const rows = [];
102
+ const ordered = options.preserveOrder
103
+ ? [...(descriptors ?? [])]
104
+ : [...(descriptors ?? [])].sort(
105
+ (left, right) => (sessionConversationUpdatedAt(right) ?? 0) - (sessionConversationUpdatedAt(left) ?? 0),
106
+ );
107
+ for (const descriptor of ordered) {
108
+ if (!descriptor?.locator) continue;
109
+ const key = options.keyFor(descriptor);
110
+ if (typeof key !== 'string' || !key || seen.has(key)) continue;
111
+ seen.add(key);
112
+ const preview = descriptorPreview(descriptor);
113
+ const updatedAt = typeof descriptor.updated_at_ms === 'number' ? descriptor.updated_at_ms : null;
114
+ rows.push({
115
+ key,
116
+ harness: descriptor.locator.harness,
117
+ name: workspaceName(descriptor.cwd) || 'no workspace',
118
+ cwd: shortWorkspacePath(descriptor.cwd, options.home),
119
+ title: descriptorTitle(descriptor),
120
+ preview: preview?.text ?? '',
121
+ age: relativeAge(preview?.updatedAt ?? updatedAt, now),
122
+ previewUpdatedAt: preview?.updatedAt ?? null,
123
+ updatedAt,
124
+ messages: typeof descriptor.message_count === 'number' ? descriptor.message_count : null,
125
+ active: active?.sessionId != null
126
+ && descriptor.locator.harness === active.harness
127
+ && descriptor.locator.session_id === active.sessionId,
128
+ live: updatedAt !== null && now - updatedAt <= liveWindowMs,
129
+ runtimeStatus: sessionDescriptorRuntimeStatus(descriptor),
130
+ });
131
+ if (rows.length >= maxSessions) break;
132
+ }
133
+ return rows;
134
+ }
135
+
31
136
  function payloadText(payload) {
32
137
  if (typeof payload === 'string') return payload;
33
138
  try {
@@ -440,7 +545,8 @@ export function createClientProjection(snapshot, options = {}) {
440
545
  };
441
546
  }
442
547
 
443
- async function dispatchStandard(controller, intent, options) {
548
+ export async function dispatchControllerIntent(controller, intent, options = {}) {
549
+ if (await options.handleIntent?.(intent)) return;
444
550
  const snapshot = controller.getSnapshot();
445
551
  const active = snapshot.activeSessionKey;
446
552
  if (intent.action === 'mounted') return;
@@ -496,7 +602,7 @@ export function createControllerBinding(controller, options = {}) {
496
602
  const adapter = {
497
603
  onIntent(intent) {
498
604
  return Promise.resolve(options.onIntent?.(intent))
499
- .then(() => dispatchStandard(controller, intent, options))
605
+ .then(() => dispatchControllerIntent(controller, intent, options))
500
606
  .catch((error) => options.onError?.(error, intent))
501
607
  .then(() => undefined);
502
608
  },
package/core.d.ts CHANGED
@@ -37,6 +37,7 @@ export {
37
37
  isSendKey,
38
38
  normalizeUiState,
39
39
  operationLabel,
40
+ parseSupercodeUiIntent,
40
41
  relativeAge,
41
42
  sessionActivity,
42
43
  sessionDisplayName,
package/core.mjs CHANGED
@@ -920,3 +920,140 @@ export function terminalCommand(handoff) {
920
920
  export function isSendKey(event) {
921
921
  return event.key === 'Enter' && !event.shiftKey && !event.isComposing;
922
922
  }
923
+
924
+ function intentRecord(value) {
925
+ return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : null;
926
+ }
927
+
928
+ function intentKeys(value, allowed) {
929
+ return Object.keys(value).every((key) => allowed.includes(key));
930
+ }
931
+
932
+ function intentContext(value) {
933
+ if (value === undefined) return [];
934
+ if (!Array.isArray(value) || value.length > 32) return null;
935
+ const items = [];
936
+ for (const candidate of value) {
937
+ const item = intentRecord(candidate);
938
+ if (!item || !intentKeys(item, ['id', 'kind', 'label', 'detail'])) return null;
939
+ if (typeof item.label !== 'string' || !item.label.trim() || item.label.length > 200) return null;
940
+ if (typeof item.detail !== 'string' || item.detail.length > 20_000) return null;
941
+ if (item.id !== undefined && (typeof item.id !== 'string' || !item.id || item.id.length > 2_000)) return null;
942
+ if (item.kind !== undefined && (typeof item.kind !== 'string' || !item.kind || item.kind.length > 100)) return null;
943
+ items.push({
944
+ ...(typeof item.id === 'string' ? { id: item.id } : {}),
945
+ ...(typeof item.kind === 'string' ? { kind: item.kind } : {}),
946
+ label: item.label.trim(),
947
+ detail: item.detail,
948
+ });
949
+ }
950
+ return items;
951
+ }
952
+
953
+ function intentImages(value) {
954
+ if (value === undefined) return [];
955
+ if (!Array.isArray(value) || value.length > 4) return null;
956
+ let totalChars = 0;
957
+ const items = [];
958
+ for (const candidate of value) {
959
+ const item = intentRecord(candidate);
960
+ if (!item || !intentKeys(item, ['id', 'label', 'url'])) return null;
961
+ if (typeof item.label !== 'string' || !item.label.trim() || item.label.length > 200) return null;
962
+ if (typeof item.url !== 'string' || !/^(?:data:image\/|https?:\/\/)/.test(item.url)) return null;
963
+ if (item.url.length > 12 * 1024 * 1024) return null;
964
+ totalChars += item.url.length;
965
+ if (totalChars > 32 * 1024 * 1024) return null;
966
+ if (item.id !== undefined && (typeof item.id !== 'string' || !item.id || item.id.length > 2_000)) return null;
967
+ items.push({ ...(typeof item.id === 'string' ? { id: item.id } : {}), label: item.label.trim(), url: item.url });
968
+ }
969
+ return items;
970
+ }
971
+
972
+ function intentJson(value, seen = new Set()) {
973
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') return true;
974
+ if (typeof value === 'number') return Number.isFinite(value);
975
+ if (typeof value !== 'object' || seen.has(value)) return false;
976
+ seen.add(value);
977
+ const valid = Array.isArray(value)
978
+ ? value.length <= 1_000 && value.every((item) => intentJson(item, seen))
979
+ : Object.keys(value).length <= 1_000 && Object.values(value).every((item) => intentJson(item, seen));
980
+ seen.delete(value);
981
+ return valid;
982
+ }
983
+
984
+ /** Runtime validation for the transport-safe intent union emitted by the default UI. */
985
+ export function parseSupercodeUiIntent(value) {
986
+ const intent = intentRecord(value);
987
+ if (!intent || typeof intent.action !== 'string') return null;
988
+ const exact = (...keys) => intentKeys(intent, ['action', ...keys]);
989
+ const actionOnly = ['mounted', 'loadSessions', 'loadEarlier', 'join', 'detach', 'terminal', 'interrupt', 'refresh', 'release'];
990
+ if (actionOnly.includes(intent.action)) return exact() ? { action: intent.action } : null;
991
+ if (intent.action === 'attach' || intent.action === 'ack') {
992
+ return exact('key') && typeof intent.key === 'string' && intent.key.trim() && intent.key.length <= 4_000
993
+ ? { action: intent.action, key: intent.key.trim() }
994
+ : null;
995
+ }
996
+ if (intent.action === 'draft') {
997
+ return exact('text') && typeof intent.text === 'string' && intent.text.length <= 50_000
998
+ ? { action: 'draft', text: intent.text }
999
+ : null;
1000
+ }
1001
+ if (intent.action === 'send' || intent.action === 'new') {
1002
+ const allowed = intent.action === 'new'
1003
+ ? exact('harness', 'mode', 'text', 'context', 'images')
1004
+ : exact('text', 'context', 'images');
1005
+ const context = intentContext(intent.context);
1006
+ const images = intentImages(intent.images);
1007
+ if (!allowed || typeof intent.text !== 'string' || context === null || images === null) return null;
1008
+ if (!intent.text.trim() && images.length === 0) return null;
1009
+ const extras = {
1010
+ ...(context.length ? { context } : {}),
1011
+ ...(images.length ? { images } : {}),
1012
+ };
1013
+ if (intent.action === 'send') return { action: 'send', text: intent.text, ...extras };
1014
+ if (typeof intent.harness !== 'string' || !intent.harness.trim() || intent.harness.length > 100) return null;
1015
+ const mode = intent.mode === undefined ? undefined : intent.mode;
1016
+ if (mode !== undefined && mode !== 'headless' && mode !== 'terminal') return null;
1017
+ if (mode === 'terminal' && !intent.text.trim()) return null;
1018
+ return { action: 'new', harness: intent.harness.trim(), ...(mode ? { mode } : {}), text: intent.text, ...extras };
1019
+ }
1020
+ if (intent.action === 'resume') {
1021
+ return exact('mode') && (intent.mode === undefined || intent.mode === 'headless' || intent.mode === 'terminal')
1022
+ ? { action: 'resume', ...(intent.mode ? { mode: intent.mode } : {}) }
1023
+ : null;
1024
+ }
1025
+ if (intent.action === 'branch' || intent.action === 'reduce') {
1026
+ if (!exact('targetHarness')) return null;
1027
+ if (intent.targetHarness === undefined) return { action: intent.action };
1028
+ return typeof intent.targetHarness === 'string' && intent.targetHarness.trim() && intent.targetHarness.length <= 100
1029
+ ? { action: intent.action, targetHarness: intent.targetHarness.trim() }
1030
+ : null;
1031
+ }
1032
+ if (intent.action === 'export') {
1033
+ return exact('targetHarness') && typeof intent.targetHarness === 'string' && intent.targetHarness.trim() && intent.targetHarness.length <= 100
1034
+ ? { action: 'export', targetHarness: intent.targetHarness.trim() }
1035
+ : null;
1036
+ }
1037
+ if (intent.action === 'respond') {
1038
+ if (!exact('requestId', 'optionId') || !intentJson(intent.requestId)) return null;
1039
+ return intent.optionId === null || typeof intent.optionId === 'string'
1040
+ ? { action: 'respond', requestId: intent.requestId, optionId: intent.optionId }
1041
+ : null;
1042
+ }
1043
+ if (intent.action === 'configureHarness') {
1044
+ if (!exact('harness', 'changes', 'expectedRevision')) return null;
1045
+ if (typeof intent.harness !== 'string' || !intent.harness || intent.harness.length > 100) return null;
1046
+ if (typeof intent.expectedRevision !== 'string' || !intent.expectedRevision || intent.expectedRevision.length > 500) return null;
1047
+ if (!Array.isArray(intent.changes) || !intent.changes.length || intent.changes.length > 20) return null;
1048
+ const changes = [];
1049
+ for (const candidate of intent.changes) {
1050
+ const change = intentRecord(candidate);
1051
+ if (!change || !intentKeys(change, ['key', 'value'])) return null;
1052
+ if (typeof change.key !== 'string' || !change.key || change.key.length > 200) return null;
1053
+ if (change.value !== null && (typeof change.value !== 'string' || change.value.length > 500)) return null;
1054
+ changes.push({ key: change.key, value: change.value });
1055
+ }
1056
+ return { action: 'configureHarness', harness: intent.harness, changes, expectedRevision: intent.expectedRevision };
1057
+ }
1058
+ return null;
1059
+ }
package/index.d.ts CHANGED
@@ -449,6 +449,8 @@ export function canContinueHere(state: SupercodeUiState): boolean;
449
449
  export function operationLabel(operation: string | null): string;
450
450
  export function terminalCommand(handoff: NonNullable<SupercodeUiState['terminalHandoff']>): string;
451
451
  export function isSendKey(event: Pick<KeyboardEvent, 'key' | 'shiftKey' | 'isComposing'>): boolean;
452
+ /** Strictly validate an unknown browser/wire payload as a bounded UI intent. */
453
+ export function parseSupercodeUiIntent(value: unknown): SupercodeUiIntent | null;
452
454
 
453
455
  export function HarnessLogo(props: { id: HarnessId; activity?: SessionActivity; size?: number; onMissingLogo?(id: string): void }): VNode | null;
454
456
  export type UiIconName = 'attach' | 'back' | 'check' | 'chevron' | 'close' | 'copy' | 'down' | 'menu' | 'plus' | 'search' | 'send' | 'stop';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter-ai-dev/supercode-ui",
3
- "version": "0.1.35",
3
+ "version": "0.1.36",
4
4
  "type": "module",
5
5
  "description": "Composable default UI kit for Supercode-powered coding-agent experiences",
6
6
  "exports": {
@@ -107,6 +107,7 @@
107
107
  }
108
108
  },
109
109
  "devDependencies": {
110
+ "@volter-ai-dev/supercode-client": "0.3.22",
110
111
  "@storybook/addon-a11y": "10.5.9",
111
112
  "@storybook/addon-docs": "10.5.9",
112
113
  "@storybook/addon-themes": "10.5.9",