@volter-ai-dev/supercode-ui 0.1.0
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 +116 -0
- package/components.mjs +963 -0
- package/composer.mjs +168 -0
- package/controller.d.ts +53 -0
- package/controller.mjs +262 -0
- package/conversation.mjs +375 -0
- package/core.mjs +349 -0
- package/embed.mjs +972 -0
- package/index.d.ts +313 -0
- package/index.mjs +3 -0
- package/logo.mjs +114 -0
- package/messenger.mjs +947 -0
- package/package.json +106 -0
- package/sessions.mjs +196 -0
- package/styles.css +154 -0
package/composer.mjs
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// src/components.jsx
|
|
2
|
+
import MarkdownIt from "markdown-it";
|
|
3
|
+
import { useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks";
|
|
4
|
+
|
|
5
|
+
// core.mjs
|
|
6
|
+
var HARNESS_NAMES = Object.freeze({
|
|
7
|
+
"claude-code": "Claude Code",
|
|
8
|
+
codex: "Codex",
|
|
9
|
+
opencode: "OpenCode",
|
|
10
|
+
pi: "Pi",
|
|
11
|
+
grok: "Grok"
|
|
12
|
+
});
|
|
13
|
+
var DEFAULT_LABELS = Object.freeze({
|
|
14
|
+
chats: "Chats",
|
|
15
|
+
newChat: "New chat",
|
|
16
|
+
searchChats: "Search chats",
|
|
17
|
+
askAgent: "Ask your agent\u2026",
|
|
18
|
+
continueHere: "Continue here",
|
|
19
|
+
joinLive: "Join live",
|
|
20
|
+
forkHere: "Fork here"
|
|
21
|
+
});
|
|
22
|
+
var EMPTY_UI_STATE = Object.freeze({
|
|
23
|
+
pill: Object.freeze({ tone: "off", label: "connecting\u2026" }),
|
|
24
|
+
startup: "connecting",
|
|
25
|
+
transcript: Object.freeze([]),
|
|
26
|
+
busy: false,
|
|
27
|
+
operation: null,
|
|
28
|
+
needsInput: false,
|
|
29
|
+
harness: "",
|
|
30
|
+
mode: "none",
|
|
31
|
+
strategy: null,
|
|
32
|
+
canSend: false,
|
|
33
|
+
canResume: false,
|
|
34
|
+
canBranch: false,
|
|
35
|
+
canAttach: false,
|
|
36
|
+
canDetach: false,
|
|
37
|
+
canOpenTerminal: false,
|
|
38
|
+
canExport: false,
|
|
39
|
+
canReduce: false,
|
|
40
|
+
canInterrupt: false,
|
|
41
|
+
canRespond: false,
|
|
42
|
+
messaging: null,
|
|
43
|
+
workspace: "",
|
|
44
|
+
taskPlan: Object.freeze({ source: "none", items: Object.freeze([]), residueCount: 0, observedAt: null }),
|
|
45
|
+
semantics: Object.freeze({ fidelity: null, residue: Object.freeze([]), residueCount: 0, parseErrors: 0, rawRecords: 0, subagents: Object.freeze([]) }),
|
|
46
|
+
terminalHandoff: null,
|
|
47
|
+
exportBackTarget: null,
|
|
48
|
+
exportReceipt: null,
|
|
49
|
+
reductionReceipt: null,
|
|
50
|
+
error: null,
|
|
51
|
+
recoverable: false,
|
|
52
|
+
harnesses: Object.freeze([]),
|
|
53
|
+
history: Object.freeze({ sessionLimit: 0, hasMoreSessions: false, transcriptLimit: 120, hasEarlier: false }),
|
|
54
|
+
savedDraft: "",
|
|
55
|
+
attention: Object.freeze([]),
|
|
56
|
+
sessions: Object.freeze([]),
|
|
57
|
+
attached: null,
|
|
58
|
+
owned: null,
|
|
59
|
+
attachError: null
|
|
60
|
+
});
|
|
61
|
+
function harnessDisplayName(id) {
|
|
62
|
+
return HARNESS_NAMES[id] ?? id;
|
|
63
|
+
}
|
|
64
|
+
function canContinueHere(state) {
|
|
65
|
+
if (state.mode !== "mirror" || state.canSend) return false;
|
|
66
|
+
const row = state.attached?.key ? state.sessions.find((item) => item.key === state.attached.key) : null;
|
|
67
|
+
return state.canResume && row?.runtimeStatus !== "busy" && row?.runtimeStatus !== "idle";
|
|
68
|
+
}
|
|
69
|
+
function isSendKey(event) {
|
|
70
|
+
return event.key === "Enter" && !event.shiftKey && !event.isComposing;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/components.jsx
|
|
74
|
+
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
|
+
var composerMemory = /* @__PURE__ */ new Map();
|
|
83
|
+
function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
|
|
84
|
+
if (state.mode !== "mirror" || state.canSend) return null;
|
|
85
|
+
const attached = state.attached;
|
|
86
|
+
const row = attached?.key ? state.sessions.find((item) => item.key === attached.key) : null;
|
|
87
|
+
const activeElsewhere = row?.runtimeStatus === "busy" || row?.runtimeStatus === "idle";
|
|
88
|
+
const resume = canContinueHere(state);
|
|
89
|
+
const join = state.canAttach;
|
|
90
|
+
const branch = state.canBranch;
|
|
91
|
+
if (!resume && !join && !branch) return /* @__PURE__ */ jsx("div", { class: "scui-continuation", children: /* @__PURE__ */ jsxs("span", { children: [
|
|
92
|
+
/* @__PURE__ */ jsx("strong", { children: activeElsewhere ? "Active elsewhere" : "Read-only" }),
|
|
93
|
+
/* @__PURE__ */ jsx("small", { children: "This session cannot be continued by an available harness." })
|
|
94
|
+
] }) });
|
|
95
|
+
return /* @__PURE__ */ jsxs("div", { class: "scui-continuation", children: [
|
|
96
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
97
|
+
/* @__PURE__ */ jsx("strong", { children: activeElsewhere ? "Active elsewhere" : "Read-only" }),
|
|
98
|
+
/* @__PURE__ */ jsx("small", { children: join ? "Join the proven live runtime without taking it over." : resume ? `Resume this ${harnessDisplayName(state.harness)} session here.` : "Start an independent continuation." })
|
|
99
|
+
] }),
|
|
100
|
+
/* @__PURE__ */ jsx("button", { type: "button", disabled: Boolean(state.operation), onClick: () => adapter.onIntent(join ? { action: "join" } : resume ? { action: "resume" } : { action: "branch" }), children: join ? labels.joinLive : resume ? labels.continueHere : labels.forkHere })
|
|
101
|
+
] });
|
|
102
|
+
}
|
|
103
|
+
function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, onPending }) {
|
|
104
|
+
const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, queue: [] };
|
|
105
|
+
const [draft, setDraft] = useState(remembered.draft);
|
|
106
|
+
const [queue, setQueue] = useState(remembered.queue);
|
|
107
|
+
const textarea = useRef(null);
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (!state.busy && state.canSend && queue.length) {
|
|
110
|
+
const [next, ...rest] = queue;
|
|
111
|
+
setQueue(rest);
|
|
112
|
+
composerMemory.set(memoryKey, { draft, queue: rest });
|
|
113
|
+
onPending?.(next);
|
|
114
|
+
adapter.onIntent({ action: "send", text: next });
|
|
115
|
+
}
|
|
116
|
+
}, [adapter, draft, memoryKey, onPending, queue, state.busy, state.canSend]);
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
const timer = setTimeout(() => adapter.onIntent({ action: "draft", text: draft }), 250);
|
|
119
|
+
return () => clearTimeout(timer);
|
|
120
|
+
}, [adapter, draft]);
|
|
121
|
+
const send = () => {
|
|
122
|
+
const text = draft.trim();
|
|
123
|
+
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
|
+
});
|
|
129
|
+
else if (state.canSend) {
|
|
130
|
+
onPending?.(text);
|
|
131
|
+
adapter.onIntent({ action: "send", text });
|
|
132
|
+
} else return;
|
|
133
|
+
setDraft("");
|
|
134
|
+
composerMemory.set(memoryKey, { draft: "", queue: state.busy ? [...queue, text] : queue });
|
|
135
|
+
};
|
|
136
|
+
return /* @__PURE__ */ jsxs("div", { class: "scui-compose", children: [
|
|
137
|
+
queue.length ? /* @__PURE__ */ jsxs("div", { class: "scui-queue", children: [
|
|
138
|
+
/* @__PURE__ */ jsxs("strong", { children: [
|
|
139
|
+
queue.length,
|
|
140
|
+
" queued"
|
|
141
|
+
] }),
|
|
142
|
+
queue.map((item, index) => /* @__PURE__ */ jsxs("span", { children: [
|
|
143
|
+
item,
|
|
144
|
+
/* @__PURE__ */ jsx("button", { "aria-label": `Remove queued message ${index + 1}`, onClick: () => setQueue((items) => items.filter((_, itemIndex) => itemIndex !== index)), children: "\xD7" })
|
|
145
|
+
] }, `${index}:${item}`))
|
|
146
|
+
] }) : null,
|
|
147
|
+
/* @__PURE__ */ jsxs("div", { class: "scui-envelope", children: [
|
|
148
|
+
/* @__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
|
+
const value = event.currentTarget.value;
|
|
150
|
+
setDraft(value);
|
|
151
|
+
composerMemory.set(memoryKey, { draft: value, queue });
|
|
152
|
+
}, onKeyDown: (event) => {
|
|
153
|
+
if (isSendKey(event)) {
|
|
154
|
+
event.preventDefault();
|
|
155
|
+
send();
|
|
156
|
+
}
|
|
157
|
+
} }),
|
|
158
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
159
|
+
state.busy ? /* @__PURE__ */ jsx("button", { class: "scui-stop", type: "button", "aria-label": "Stop agent", disabled: !state.canInterrupt, onClick: () => adapter.onIntent({ action: "interrupt" }), children: "\u25A0" }) : null,
|
|
160
|
+
/* @__PURE__ */ jsx("button", { class: "scui-send", type: "button", "aria-label": state.busy ? "Queue message" : "Send message", disabled: !draft.trim() || !state.busy && !state.canSend, onClick: send, children: state.busy ? "+" : "\u2191" })
|
|
161
|
+
] })
|
|
162
|
+
] })
|
|
163
|
+
] });
|
|
164
|
+
}
|
|
165
|
+
export {
|
|
166
|
+
Composer,
|
|
167
|
+
ContinuationBar
|
|
168
|
+
};
|
package/controller.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SessionArtifact,
|
|
3
|
+
SessionFormat,
|
|
4
|
+
} from '@volter-ai-dev/supercode-harness-sdk';
|
|
5
|
+
import type {
|
|
6
|
+
SupercodeClientSnapshot,
|
|
7
|
+
SupercodeController,
|
|
8
|
+
} from '@volter-ai-dev/supercode-client';
|
|
9
|
+
import type {
|
|
10
|
+
SessionAttention,
|
|
11
|
+
SupercodeUiIntent,
|
|
12
|
+
SupercodeUiState,
|
|
13
|
+
UiAdapter,
|
|
14
|
+
} from './index.js';
|
|
15
|
+
|
|
16
|
+
export interface ClientProjectionOptions {
|
|
17
|
+
now?: number;
|
|
18
|
+
attention?: SessionAttention[];
|
|
19
|
+
savedDraft?: string;
|
|
20
|
+
history?: Partial<SupercodeUiState['history']>;
|
|
21
|
+
exportBackTarget?: SessionFormat | null;
|
|
22
|
+
exportReceipt?: SupercodeUiState['exportReceipt'];
|
|
23
|
+
reductionReceipt?: SupercodeUiState['reductionReceipt'];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function projectClientSnapshot(
|
|
27
|
+
snapshot: SupercodeClientSnapshot,
|
|
28
|
+
options?: ClientProjectionOptions,
|
|
29
|
+
): SupercodeUiState;
|
|
30
|
+
|
|
31
|
+
export interface ControllerBindingOptions {
|
|
32
|
+
projection?: () => ClientProjectionOptions;
|
|
33
|
+
onIntent?: (intent: SupercodeUiIntent) => void | Promise<void>;
|
|
34
|
+
onUnsupported?: (intent: SupercodeUiIntent) => void | Promise<void>;
|
|
35
|
+
onError?: (error: unknown, intent: SupercodeUiIntent) => void | Promise<void>;
|
|
36
|
+
onArtifact?: (artifact: SessionArtifact, intent: Extract<SupercodeUiIntent, { action: 'export' }>) => void | Promise<void>;
|
|
37
|
+
onDraft?: (text: string) => void | Promise<void>;
|
|
38
|
+
onAcknowledge?: (key: string) => void | Promise<void>;
|
|
39
|
+
onLoadSessions?: () => void | Promise<void>;
|
|
40
|
+
onLoadEarlier?: () => void | Promise<void>;
|
|
41
|
+
copyText?: UiAdapter['copyText'];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface SupercodeUiBinding {
|
|
45
|
+
adapter: UiAdapter;
|
|
46
|
+
getState(): SupercodeUiState;
|
|
47
|
+
subscribe(listener: (state: SupercodeUiState) => void): () => void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function createControllerBinding(
|
|
51
|
+
controller: SupercodeController,
|
|
52
|
+
options?: ControllerBindingOptions,
|
|
53
|
+
): SupercodeUiBinding;
|
package/controller.mjs
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { normalizeUiState } from './core.mjs';
|
|
2
|
+
|
|
3
|
+
const HARNESS_LABELS = {
|
|
4
|
+
'claude-code': 'Claude Code',
|
|
5
|
+
codex: 'Codex',
|
|
6
|
+
opencode: 'OpenCode',
|
|
7
|
+
pi: 'Pi',
|
|
8
|
+
grok: 'Grok',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function label(id, supplied) {
|
|
12
|
+
return supplied || HARNESS_LABELS[id] || id;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function workspaceName(cwd) {
|
|
16
|
+
if (!cwd) return '';
|
|
17
|
+
return cwd.replaceAll('\\', '/').split('/').filter(Boolean).at(-1) ?? cwd;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function relativeAge(updatedAt, now) {
|
|
21
|
+
if (typeof updatedAt !== 'number' || !Number.isFinite(updatedAt) || updatedAt <= 0) return '';
|
|
22
|
+
const delta = Math.max(0, now - updatedAt);
|
|
23
|
+
if (delta < 60_000) return 'now';
|
|
24
|
+
if (delta < 3_600_000) return `${Math.floor(delta / 60_000)}m ago`;
|
|
25
|
+
if (delta < 86_400_000) return `${Math.floor(delta / 3_600_000)}h ago`;
|
|
26
|
+
if (delta < 604_800_000) return `${Math.floor(delta / 86_400_000)}d ago`;
|
|
27
|
+
return `${Math.floor(delta / 604_800_000)}w ago`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function payloadText(payload) {
|
|
31
|
+
if (typeof payload === 'string') return payload;
|
|
32
|
+
try {
|
|
33
|
+
return JSON.stringify(payload, null, 2);
|
|
34
|
+
} catch {
|
|
35
|
+
return String(payload ?? '');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
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
|
+
}];
|
|
51
|
+
}
|
|
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 ?? '',
|
|
62
|
+
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
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function attached(snapshot) {
|
|
92
|
+
const session = snapshot.sessions.find((candidate) => candidate.key === snapshot.activeSessionKey);
|
|
93
|
+
if (!session) return snapshot.activeHarness ? {
|
|
94
|
+
key: snapshot.activeSessionKey ?? '',
|
|
95
|
+
harness: snapshot.activeHarness,
|
|
96
|
+
name: workspaceName(snapshot.workspace),
|
|
97
|
+
cwd: snapshot.workspace,
|
|
98
|
+
title: snapshot.activeSession?.model ?? snapshot.activeSessionId ?? 'Active session',
|
|
99
|
+
} : null;
|
|
100
|
+
return {
|
|
101
|
+
key: session.key,
|
|
102
|
+
harness: session.harness,
|
|
103
|
+
name: workspaceName(session.cwd),
|
|
104
|
+
cwd: session.cwd ?? '',
|
|
105
|
+
title: session.title ?? session.model ?? session.sessionId,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function semantics(session) {
|
|
110
|
+
if (!session) return { fidelity: null, residue: [], residueCount: 0, parseErrors: 0, rawRecords: 0, subagents: [] };
|
|
111
|
+
return {
|
|
112
|
+
fidelity: session.fidelity ?? null,
|
|
113
|
+
residue: Array.isArray(session.residue) ? session.residue : [],
|
|
114
|
+
residueCount: Array.isArray(session.residue) ? session.residue.length : 0,
|
|
115
|
+
parseErrors: Number.isFinite(session.parse_error_lines) ? session.parse_error_lines : 0,
|
|
116
|
+
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}`,
|
|
119
|
+
source: child.source,
|
|
120
|
+
model: child.model ?? null,
|
|
121
|
+
messages: Array.isArray(child.messages) ? child.messages.length : 0,
|
|
122
|
+
fidelity: child.fidelity ?? 'semantic',
|
|
123
|
+
})) : [],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function projectClientSnapshot(snapshot, options = {}) {
|
|
128
|
+
const now = Number.isFinite(options.now) ? options.now : Date.now();
|
|
129
|
+
const busy = snapshot.turn?.state && snapshot.turn.state !== 'idle';
|
|
130
|
+
const actions = snapshot.availableActions ?? {};
|
|
131
|
+
const active = attached(snapshot);
|
|
132
|
+
const sessions = (snapshot.sessions ?? []).map((session) => ({
|
|
133
|
+
key: session.key,
|
|
134
|
+
harness: session.harness,
|
|
135
|
+
name: workspaceName(session.cwd),
|
|
136
|
+
cwd: session.cwd ?? '',
|
|
137
|
+
title: session.title ?? session.model ?? session.sessionId,
|
|
138
|
+
age: relativeAge(session.updatedAt, now),
|
|
139
|
+
updatedAt: session.updatedAt ?? null,
|
|
140
|
+
messages: session.messageCount ?? null,
|
|
141
|
+
active: session.key === snapshot.activeSessionKey,
|
|
142
|
+
live: typeof session.updatedAt === 'number' && now - session.updatedAt <= 300_000,
|
|
143
|
+
runtimeStatus: session.liveStatus ?? null,
|
|
144
|
+
}));
|
|
145
|
+
const startup = snapshot.availability === 'loading'
|
|
146
|
+
? snapshot.operation === 'start' ? 'starting' : snapshot.operation === 'refresh' ? 'discovering' : 'connecting'
|
|
147
|
+
: 'ready';
|
|
148
|
+
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 ?? []),
|
|
156
|
+
busy,
|
|
157
|
+
operation: snapshot.operation,
|
|
158
|
+
needsInput: Array.isArray(snapshot.requests) && snapshot.requests.some((request) => request.status === 'pending'),
|
|
159
|
+
harness: snapshot.activeHarness ?? '',
|
|
160
|
+
mode: snapshot.connection?.mode ?? 'none',
|
|
161
|
+
strategy: snapshot.connection?.strategy ?? null,
|
|
162
|
+
canSend: actions.send === true,
|
|
163
|
+
canResume: actions.resume === true,
|
|
164
|
+
canBranch: actions.branch === true,
|
|
165
|
+
canAttach: actions.attach === true,
|
|
166
|
+
canDetach: actions.detach === true,
|
|
167
|
+
canOpenTerminal: actions.openTerminal === true,
|
|
168
|
+
canExport: Boolean(options.exportBackTarget && snapshot.activeSessionKey),
|
|
169
|
+
canReduce: false,
|
|
170
|
+
canInterrupt: actions.interrupt === true,
|
|
171
|
+
canRespond: actions.respond === true,
|
|
172
|
+
messaging: snapshot.connection?.messaging ?? null,
|
|
173
|
+
workspace: snapshot.workspace ?? '',
|
|
174
|
+
taskPlan: {
|
|
175
|
+
source: snapshot.taskPlan?.source ?? 'none',
|
|
176
|
+
items: snapshot.taskPlan?.items ?? [],
|
|
177
|
+
residueCount: snapshot.taskPlan?.residue?.length ?? 0,
|
|
178
|
+
observedAt: snapshot.taskPlan?.observedAt ?? null,
|
|
179
|
+
},
|
|
180
|
+
semantics: semantics(snapshot.activeSession),
|
|
181
|
+
terminalHandoff: snapshot.terminalLaunch ? {
|
|
182
|
+
program: snapshot.terminalLaunch.program,
|
|
183
|
+
arguments: snapshot.terminalLaunch.arguments,
|
|
184
|
+
cwd: snapshot.terminalLaunch.cwd,
|
|
185
|
+
} : null,
|
|
186
|
+
exportBackTarget: options.exportBackTarget ?? null,
|
|
187
|
+
exportReceipt: options.exportReceipt ?? null,
|
|
188
|
+
reductionReceipt: options.reductionReceipt ?? null,
|
|
189
|
+
error: snapshot.error?.message ?? null,
|
|
190
|
+
recoverable: snapshot.error?.recoverable === true,
|
|
191
|
+
harnesses: (snapshot.harnesses ?? []).map((harness) => ({
|
|
192
|
+
id: harness.id,
|
|
193
|
+
label: label(harness.id, harness.display_name),
|
|
194
|
+
installed: harness.installed === true,
|
|
195
|
+
startable: harness.availableActions?.start === true,
|
|
196
|
+
reason: harness.reason ?? null,
|
|
197
|
+
})),
|
|
198
|
+
history: {
|
|
199
|
+
sessionLimit: options.history?.sessionLimit ?? sessions.length,
|
|
200
|
+
hasMoreSessions: options.history?.hasMoreSessions ?? false,
|
|
201
|
+
transcriptLimit: options.history?.transcriptLimit ?? Math.max(120, snapshot.conversation?.length ?? 0),
|
|
202
|
+
hasEarlier: options.history?.hasEarlier ?? false,
|
|
203
|
+
},
|
|
204
|
+
savedDraft: options.savedDraft ?? '',
|
|
205
|
+
attention: options.attention ?? [],
|
|
206
|
+
sessions,
|
|
207
|
+
attached: active,
|
|
208
|
+
owned: snapshot.connection?.ownsRuntime ? active : null,
|
|
209
|
+
attachError: null,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function dispatchStandard(controller, intent, options) {
|
|
214
|
+
const snapshot = controller.getSnapshot();
|
|
215
|
+
const active = snapshot.activeSessionKey;
|
|
216
|
+
if (intent.action === 'mounted') return;
|
|
217
|
+
if (intent.action === 'attach') return controller.dispatch({ type: 'observe', sessionKey: intent.key });
|
|
218
|
+
if (intent.action === 'send') return controller.dispatch({ type: 'send', text: intent.text });
|
|
219
|
+
if (intent.action === 'new') {
|
|
220
|
+
await controller.dispatch({ type: 'start', harness: intent.harness });
|
|
221
|
+
return controller.dispatch({ type: 'send', text: intent.text });
|
|
222
|
+
}
|
|
223
|
+
if (intent.action === 'resume' && active) return controller.dispatch({ type: 'resume', sessionKey: active });
|
|
224
|
+
if (intent.action === 'join' && active) return controller.dispatch({ type: 'attach', sessionKey: active });
|
|
225
|
+
if (intent.action === 'detach') return controller.dispatch({ type: 'detach' });
|
|
226
|
+
if (intent.action === 'branch' && active) return controller.dispatch({ type: 'branch', sessionKey: active, ...(intent.targetHarness ? { targetHarness: intent.targetHarness } : {}) });
|
|
227
|
+
if (intent.action === 'terminal') return controller.dispatch({ type: 'openTerminal' });
|
|
228
|
+
if (intent.action === 'interrupt') return controller.dispatch({ type: 'interrupt' });
|
|
229
|
+
if (intent.action === 'respond') return controller.dispatch({ type: 'respond', requestId: intent.requestId, optionId: intent.optionId });
|
|
230
|
+
if (intent.action === 'refresh') return controller.dispatch({ type: 'refresh', autoObserve: true });
|
|
231
|
+
if (intent.action === 'export' && active) {
|
|
232
|
+
const artifact = await controller.exportSession(active, intent.targetHarness);
|
|
233
|
+
return options.onArtifact?.(artifact, intent);
|
|
234
|
+
}
|
|
235
|
+
if (intent.action === 'draft') return options.onDraft?.(intent.text);
|
|
236
|
+
if (intent.action === 'ack') return options.onAcknowledge?.(intent.key);
|
|
237
|
+
if (intent.action === 'loadSessions') return options.onLoadSessions?.();
|
|
238
|
+
if (intent.action === 'loadEarlier') return options.onLoadEarlier?.();
|
|
239
|
+
return options.onUnsupported?.(intent);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function createControllerBinding(controller, options = {}) {
|
|
243
|
+
if (!controller || typeof controller.getSnapshot !== 'function' || typeof controller.subscribe !== 'function' || typeof controller.dispatch !== 'function') {
|
|
244
|
+
throw new TypeError('createControllerBinding requires a SupercodeController-compatible object');
|
|
245
|
+
}
|
|
246
|
+
const getState = () => projectClientSnapshot(controller.getSnapshot(), options.projection?.() ?? {});
|
|
247
|
+
const adapter = {
|
|
248
|
+
onIntent(intent) {
|
|
249
|
+
void Promise.resolve(options.onIntent?.(intent))
|
|
250
|
+
.then(() => dispatchStandard(controller, intent, options))
|
|
251
|
+
.catch((error) => options.onError?.(error, intent));
|
|
252
|
+
},
|
|
253
|
+
...(options.copyText ? { copyText: options.copyText } : {}),
|
|
254
|
+
};
|
|
255
|
+
return {
|
|
256
|
+
adapter,
|
|
257
|
+
getState,
|
|
258
|
+
subscribe(listener) {
|
|
259
|
+
return controller.subscribe(() => listener(getState()));
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
}
|