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

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/core.mjs CHANGED
@@ -30,6 +30,7 @@ export const EMPTY_UI_STATE = Object.freeze({
30
30
  mode: 'none',
31
31
  strategy: null,
32
32
  canSend: false,
33
+ canSteer: false,
33
34
  canResume: false,
34
35
  continuationModes: Object.freeze([]),
35
36
  canBranch: false,
@@ -771,6 +772,7 @@ export function normalizeUiState(value) {
771
772
  mode: MODES.has(raw.mode) ? raw.mode : 'none',
772
773
  strategy: STRATEGIES.has(raw.strategy) ? raw.strategy : null,
773
774
  canSend: raw.canSend === true,
775
+ canSteer: raw.canSteer === true,
774
776
  canResume,
775
777
  continuationModes,
776
778
  canBranch: raw.canBranch === true,
@@ -804,7 +806,29 @@ export function normalizeUiState(value) {
804
806
  const preferredLaunchMode = launchModes.includes(item.preferredLaunchMode)
805
807
  ? item.preferredLaunchMode
806
808
  : launchModes[0] ?? null;
807
- return [{ id: item.id, label: string(item.label, harnessDisplayName(item.id)), installed: item.installed === true, startable, reason: typeof item.reason === 'string' ? item.reason : null, launchModes, preferredLaunchMode }];
809
+ const capabilities = record(item.capabilities);
810
+ return [{
811
+ id: item.id,
812
+ label: string(item.label, harnessDisplayName(item.id)),
813
+ installed: item.installed === true,
814
+ startable,
815
+ reason: typeof item.reason === 'string' ? boundedString(item.reason) : null,
816
+ auth: ['ready', 'configured', 'required', 'unknown'].includes(item.auth) ? item.auth : 'unknown',
817
+ runtime: ['ready', 'degraded', 'unavailable'].includes(item.runtime) ? item.runtime : startable ? 'ready' : 'unavailable',
818
+ protocol: boundedString(item.protocol, 100),
819
+ repair: typeof item.repair === 'string' ? boundedString(item.repair, 4_000) : null,
820
+ capabilities: {
821
+ start: capabilities?.start === true,
822
+ resume: capabilities?.resume === true,
823
+ attach: capabilities?.attach === true,
824
+ send: capabilities?.send === true,
825
+ interrupt: capabilities?.interrupt === true,
826
+ steer: capabilities?.steer === true,
827
+ respond: capabilities?.respond === true,
828
+ },
829
+ launchModes,
830
+ preferredLaunchMode,
831
+ }];
808
832
  }) : [],
809
833
  history: { sessionLimit: number(history?.sessionLimit), hasMoreSessions: history?.hasMoreSessions === true, transcriptLimit: number(history?.transcriptLimit, 120), hasEarlier: history?.hasEarlier === true },
810
834
  savedDraft: string(raw.savedDraft),
@@ -843,6 +867,60 @@ export function sessionActivity(state, row) {
843
867
  return 'idle';
844
868
  }
845
869
 
870
+ const ACTIVITY_PRIORITY = Object.freeze({
871
+ 'needs-input': 70,
872
+ failed: 60,
873
+ working: 50,
874
+ unseen: 40,
875
+ finished: 30,
876
+ running: 20,
877
+ recent: 10,
878
+ idle: 0,
879
+ });
880
+
881
+ function activityDetail(activity, preview, pill) {
882
+ if (preview) return preview;
883
+ if (activity === 'needs-input') return 'Needs input';
884
+ if (activity === 'failed') return 'Run failed';
885
+ if (activity === 'working') return 'Working…';
886
+ if (activity === 'finished') return 'Finished';
887
+ if (activity === 'unseen') return 'New reply';
888
+ if (activity === 'running') return 'Session is running';
889
+ return pill || 'Agent chats';
890
+ }
891
+
892
+ /** A stable, host-agnostic projection for overlay launchers and compact activity trays. */
893
+ export function projectAgentActivity(value) {
894
+ const state = normalizeUiState(value);
895
+ const attentionByKey = new Map(state.attention.map((item) => [item.key, item]));
896
+ const unreadCount = state.attention.reduce((total, item) => total + (item.unreadCount ?? 1), 0);
897
+ const ranked = state.sessions.map((row, index) => {
898
+ const activity = sessionActivity(state, row);
899
+ return {
900
+ row,
901
+ index,
902
+ activity,
903
+ priority: ACTIVITY_PRIORITY[activity] ?? 0,
904
+ active: row.active || row.key === state.attached?.key || row.key === state.owned?.key,
905
+ updatedAt: row.previewUpdatedAt ?? row.updatedAt ?? 0,
906
+ };
907
+ }).sort((left, right) => right.priority - left.priority || Number(right.active) - Number(left.active) || right.updatedAt - left.updatedAt || left.index - right.index);
908
+ const selected = ranked[0];
909
+ const fallback = state.attached ?? state.owned;
910
+ const harness = selected?.row.harness ?? fallback?.harness ?? state.harness;
911
+ const activity = selected?.activity ?? (state.needsInput ? 'needs-input' : state.busy ? 'working' : state.startup === 'ready' ? 'idle' : 'recent');
912
+ const attention = selected ? attentionByKey.get(selected.row.key) : null;
913
+ const preview = selected?.row.preview || attention?.preview || '';
914
+ return {
915
+ harness,
916
+ sessionKey: selected?.row.key ?? fallback?.key ?? null,
917
+ title: selected ? sessionDisplayName(selected.row) : fallback ? sessionDisplayName(fallback) : 'Agent chats',
918
+ detail: activityDetail(activity, preview, state.pill.label),
919
+ activity,
920
+ unreadCount,
921
+ };
922
+ }
923
+
846
924
  export function filterSessions(rows, query) {
847
925
  const needle = query.trim().toLocaleLowerCase();
848
926
  if (!needle) return [...rows];
@@ -908,7 +986,7 @@ export function canContinueHere(state) {
908
986
 
909
987
  export function operationLabel(operation) {
910
988
  if (!operation) return '';
911
- const labels = { discover: 'Refreshing chats…', observe: 'Loading recent messages…', attach: 'Opening chat…', start: 'Starting new chat…', resume: 'Continuing here…', join: 'Joining live session…', detach: 'Detaching to read-only…', branch: 'Starting continuation…', reduce: 'Reducing context and verifying reversibility…', terminal: 'Preparing terminal handoff…', export: 'Exporting losslessly…', interrupt: 'Stopping agent…', respond: 'Sending response…', configureHarness: 'Updating harness settings…', loadEarlier: 'Loading earlier messages…', loadSessions: 'Loading more chats…', refresh: 'Retrying…' };
989
+ const labels = { discover: 'Refreshing chats…', observe: 'Loading recent messages…', attach: 'Opening chat…', start: 'Starting new chat…', resume: 'Continuing here…', join: 'Joining live session…', detach: 'Detaching to read-only…', branch: 'Starting continuation…', reduce: 'Reducing context and verifying reversibility…', terminal: 'Preparing terminal handoff…', export: 'Exporting losslessly…', steer: 'Steering agent…', interrupt: 'Stopping agent…', respond: 'Sending response…', configureHarness: 'Updating harness settings…', loadEarlier: 'Loading earlier messages…', loadSessions: 'Loading more chats…', refresh: 'Retrying…' };
912
990
  return labels[operation] ?? `${operation.replaceAll('_', ' ')}…`;
913
991
  }
914
992
 
@@ -920,3 +998,145 @@ export function terminalCommand(handoff) {
920
998
  export function isSendKey(event) {
921
999
  return event.key === 'Enter' && !event.shiftKey && !event.isComposing;
922
1000
  }
1001
+
1002
+ function intentRecord(value) {
1003
+ return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : null;
1004
+ }
1005
+
1006
+ function intentKeys(value, allowed) {
1007
+ return Object.keys(value).every((key) => allowed.includes(key));
1008
+ }
1009
+
1010
+ function intentContext(value) {
1011
+ if (value === undefined) return [];
1012
+ if (!Array.isArray(value) || value.length > 32) return null;
1013
+ const items = [];
1014
+ for (const candidate of value) {
1015
+ const item = intentRecord(candidate);
1016
+ if (!item || !intentKeys(item, ['id', 'kind', 'label', 'detail'])) return null;
1017
+ if (typeof item.label !== 'string' || !item.label.trim() || item.label.length > 200) return null;
1018
+ if (typeof item.detail !== 'string' || item.detail.length > 20_000) return null;
1019
+ if (item.id !== undefined && (typeof item.id !== 'string' || !item.id || item.id.length > 2_000)) return null;
1020
+ if (item.kind !== undefined && (typeof item.kind !== 'string' || !item.kind || item.kind.length > 100)) return null;
1021
+ items.push({
1022
+ ...(typeof item.id === 'string' ? { id: item.id } : {}),
1023
+ ...(typeof item.kind === 'string' ? { kind: item.kind } : {}),
1024
+ label: item.label.trim(),
1025
+ detail: item.detail,
1026
+ });
1027
+ }
1028
+ return items;
1029
+ }
1030
+
1031
+ function intentImages(value) {
1032
+ if (value === undefined) return [];
1033
+ if (!Array.isArray(value) || value.length > 4) return null;
1034
+ let totalChars = 0;
1035
+ const items = [];
1036
+ for (const candidate of value) {
1037
+ const item = intentRecord(candidate);
1038
+ if (!item || !intentKeys(item, ['id', 'label', 'url'])) return null;
1039
+ if (typeof item.label !== 'string' || !item.label.trim() || item.label.length > 200) return null;
1040
+ if (typeof item.url !== 'string' || !/^(?:data:image\/|https?:\/\/)/.test(item.url)) return null;
1041
+ if (item.url.length > 12 * 1024 * 1024) return null;
1042
+ totalChars += item.url.length;
1043
+ if (totalChars > 32 * 1024 * 1024) return null;
1044
+ if (item.id !== undefined && (typeof item.id !== 'string' || !item.id || item.id.length > 2_000)) return null;
1045
+ items.push({ ...(typeof item.id === 'string' ? { id: item.id } : {}), label: item.label.trim(), url: item.url });
1046
+ }
1047
+ return items;
1048
+ }
1049
+
1050
+ function intentJson(value, seen = new Set()) {
1051
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') return true;
1052
+ if (typeof value === 'number') return Number.isFinite(value);
1053
+ if (typeof value !== 'object' || seen.has(value)) return false;
1054
+ seen.add(value);
1055
+ const valid = Array.isArray(value)
1056
+ ? value.length <= 1_000 && value.every((item) => intentJson(item, seen))
1057
+ : Object.keys(value).length <= 1_000 && Object.values(value).every((item) => intentJson(item, seen));
1058
+ seen.delete(value);
1059
+ return valid;
1060
+ }
1061
+
1062
+ /** Runtime validation for the transport-safe intent union emitted by the default UI. */
1063
+ export function parseSupercodeUiIntent(value) {
1064
+ const intent = intentRecord(value);
1065
+ if (!intent || typeof intent.action !== 'string') return null;
1066
+ const exact = (...keys) => intentKeys(intent, ['action', ...keys]);
1067
+ const actionOnly = ['mounted', 'loadSessions', 'loadEarlier', 'join', 'detach', 'terminal', 'interrupt', 'refresh', 'release'];
1068
+ if (actionOnly.includes(intent.action)) return exact() ? { action: intent.action } : null;
1069
+ if (intent.action === 'attach' || intent.action === 'ack') {
1070
+ return exact('key') && typeof intent.key === 'string' && intent.key.trim() && intent.key.length <= 4_000
1071
+ ? { action: intent.action, key: intent.key.trim() }
1072
+ : null;
1073
+ }
1074
+ if (intent.action === 'draft') {
1075
+ return exact('text') && typeof intent.text === 'string' && intent.text.length <= 50_000
1076
+ ? { action: 'draft', text: intent.text }
1077
+ : null;
1078
+ }
1079
+ if (intent.action === 'steer') {
1080
+ return exact('text') && typeof intent.text === 'string' && intent.text.trim() && intent.text.length <= 50_000
1081
+ ? { action: 'steer', text: intent.text }
1082
+ : null;
1083
+ }
1084
+ if (intent.action === 'send' || intent.action === 'new') {
1085
+ const allowed = intent.action === 'new'
1086
+ ? exact('harness', 'mode', 'text', 'context', 'images')
1087
+ : exact('text', 'context', 'images');
1088
+ const context = intentContext(intent.context);
1089
+ const images = intentImages(intent.images);
1090
+ if (!allowed || typeof intent.text !== 'string' || context === null || images === null) return null;
1091
+ if (!intent.text.trim() && images.length === 0) return null;
1092
+ const extras = {
1093
+ ...(context.length ? { context } : {}),
1094
+ ...(images.length ? { images } : {}),
1095
+ };
1096
+ if (intent.action === 'send') return { action: 'send', text: intent.text, ...extras };
1097
+ if (typeof intent.harness !== 'string' || !intent.harness.trim() || intent.harness.length > 100) return null;
1098
+ const mode = intent.mode === undefined ? undefined : intent.mode;
1099
+ if (mode !== undefined && mode !== 'headless' && mode !== 'terminal') return null;
1100
+ if (mode === 'terminal' && !intent.text.trim()) return null;
1101
+ return { action: 'new', harness: intent.harness.trim(), ...(mode ? { mode } : {}), text: intent.text, ...extras };
1102
+ }
1103
+ if (intent.action === 'resume') {
1104
+ return exact('mode') && (intent.mode === undefined || intent.mode === 'headless' || intent.mode === 'terminal')
1105
+ ? { action: 'resume', ...(intent.mode ? { mode: intent.mode } : {}) }
1106
+ : null;
1107
+ }
1108
+ if (intent.action === 'branch' || intent.action === 'reduce') {
1109
+ if (!exact('targetHarness')) return null;
1110
+ if (intent.targetHarness === undefined) return { action: intent.action };
1111
+ return typeof intent.targetHarness === 'string' && intent.targetHarness.trim() && intent.targetHarness.length <= 100
1112
+ ? { action: intent.action, targetHarness: intent.targetHarness.trim() }
1113
+ : null;
1114
+ }
1115
+ if (intent.action === 'export') {
1116
+ return exact('targetHarness') && typeof intent.targetHarness === 'string' && intent.targetHarness.trim() && intent.targetHarness.length <= 100
1117
+ ? { action: 'export', targetHarness: intent.targetHarness.trim() }
1118
+ : null;
1119
+ }
1120
+ if (intent.action === 'respond') {
1121
+ if (!exact('requestId', 'optionId') || !intentJson(intent.requestId)) return null;
1122
+ return intent.optionId === null || typeof intent.optionId === 'string'
1123
+ ? { action: 'respond', requestId: intent.requestId, optionId: intent.optionId }
1124
+ : null;
1125
+ }
1126
+ if (intent.action === 'configureHarness') {
1127
+ if (!exact('harness', 'changes', 'expectedRevision')) return null;
1128
+ if (typeof intent.harness !== 'string' || !intent.harness || intent.harness.length > 100) return null;
1129
+ if (typeof intent.expectedRevision !== 'string' || !intent.expectedRevision || intent.expectedRevision.length > 500) return null;
1130
+ if (!Array.isArray(intent.changes) || !intent.changes.length || intent.changes.length > 20) return null;
1131
+ const changes = [];
1132
+ for (const candidate of intent.changes) {
1133
+ const change = intentRecord(candidate);
1134
+ if (!change || !intentKeys(change, ['key', 'value'])) return null;
1135
+ if (typeof change.key !== 'string' || !change.key || change.key.length > 200) return null;
1136
+ if (change.value !== null && (typeof change.value !== 'string' || change.value.length > 500)) return null;
1137
+ changes.push({ key: change.key, value: change.value });
1138
+ }
1139
+ return { action: 'configureHarness', harness: intent.harness, changes, expectedRevision: intent.expectedRevision };
1140
+ }
1141
+ return null;
1142
+ }