consortium
Version:
Remote control and session sharing CLI for AI coding agents
271 lines (266 loc) • 11 kB
JavaScript
'use strict';
var killSwitch = require('./killSwitch-DwcqKgA9.cjs');
var persistence = require('./types-B_i6lpTn.cjs');
var capabilities = require('./capabilities-BsNjrlBG.cjs');
var React = require('react');
var ink = require('ink');
function createRunnerAttachments(opts) {
const tag = opts.logTag ?? "runner";
let driveClient = null;
if (opts.credentials.encryption?.type === "dataKey") {
driveClient = new killSwitch.DriveClient(
opts.credentials.token,
{ x25519SecretKey: opts.credentials.encryption.machineKey }
);
}
const scratchDir = new killSwitch.ScratchDir(opts.sessionId);
const resolver = driveClient ? new killSwitch.AttachmentResolver(driveClient, scratchDir) : null;
const pendingMessageLocalIds = [];
const pendingDriveNodeRefs = [];
const pendingAttachmentIds = [];
return {
get enabled() {
return resolver !== null;
},
bindBackend(backend) {
try {
backend.setScratchDir?.(scratchDir);
} catch (err) {
persistence.logger.debug(`[${tag}/attachments] bindBackend failed:`, err);
}
},
observeUserMessage(message) {
const env = killSwitch.normalizeAttachmentEnvelope(message);
if (driveClient && env.driveId && env.driveDek) {
try {
driveClient.setKnownDek(
env.driveId,
new Uint8Array(Buffer.from(env.driveDek, "base64"))
);
} catch (err) {
persistence.logger.debug(`[${tag}/attachments] setKnownDek failed:`, err);
}
}
if (env.messageLocalId) pendingMessageLocalIds.push(env.messageLocalId);
if (env.driveId && env.driveNodeIds.length > 0) {
pendingDriveNodeRefs.push({ driveId: env.driveId, nodeIds: env.driveNodeIds });
}
if (env.bindingOnlyNodeIds.length > 0) {
pendingAttachmentIds.push(...env.bindingOnlyNodeIds);
}
},
async sendPrompt(backend, acpSessionId, text) {
let resolved = [];
if (killSwitch.attachmentsKillSwitchOff()) {
pendingMessageLocalIds.length = 0;
pendingDriveNodeRefs.length = 0;
pendingAttachmentIds.length = 0;
persistence.logger.debug(`[${tag}/attachments] kill switch CONSORTIUM_ATTACHMENTS_V1 is off \u2014 dropping attachments`);
await backend.sendPrompt(acpSessionId, text);
return;
}
if (resolver && driveClient) {
const localIds = pendingMessageLocalIds.splice(0);
const refs = pendingDriveNodeRefs.splice(0);
const ids = pendingAttachmentIds.splice(0);
try {
for (const lid of localIds) {
resolved.push(...await resolver.resolveForMessage(opts.sessionId, lid));
}
for (const ref of refs) {
resolved.push(...await resolver.resolveByDriveNodeIds(ref.driveId, ref.nodeIds));
}
if (ids.length > 0) {
try {
const bindings = await driveClient.listSessionAttachments(opts.sessionId);
const want = new Set(ids);
const matched = bindings.filter((b) => want.has(b.driveNodeId));
const byDrive = /* @__PURE__ */ new Map();
for (const b of matched) {
if (!b.driveId) continue;
const arr = byDrive.get(b.driveId) ?? [];
arr.push(b.driveNodeId);
byDrive.set(b.driveId, arr);
}
for (const [driveId, nodeIds] of byDrive) {
resolved.push(...await resolver.resolveByDriveNodeIds(driveId, nodeIds));
}
} catch (err) {
persistence.logger.debug(`[${tag}/attachments] resolve attachmentIds via bindings failed:`, err);
}
}
} catch (err) {
persistence.logger.debug(`[${tag}/attachments] resolve failed (continuing without):`, err);
resolved = [];
}
} else {
pendingMessageLocalIds.length = 0;
pendingDriveNodeRefs.length = 0;
pendingAttachmentIds.length = 0;
}
if (backend.setAttachmentCapability && opts.agentId) {
const model = opts.getCurrentModel?.() ?? null;
try {
backend.setAttachmentCapability(capabilities.resolveCapability(opts.agentId, model));
} catch (err) {
persistence.logger.debug(`[${tag}/attachments] resolveCapability failed:`, err);
}
}
if (resolved.length > 0) {
await backend.sendPrompt(acpSessionId, { text, attachments: resolved });
} else {
await backend.sendPrompt(acpSessionId, text);
}
},
async dispose() {
try {
await scratchDir.cleanup();
} catch {
}
}
};
}
const GeminiDisplay = ({ messageBuffer, logPath, currentModel, onExit }) => {
const [messages, setMessages] = React.useState([]);
const [confirmationMode, setConfirmationMode] = React.useState(false);
const [actionInProgress, setActionInProgress] = React.useState(false);
const [model, setModel] = React.useState(currentModel);
const confirmationTimeoutRef = React.useRef(null);
const { stdout } = ink.useStdout();
const terminalWidth = stdout.columns || 80;
const terminalHeight = stdout.rows || 24;
React.useEffect(() => {
if (currentModel !== void 0 && currentModel !== model) {
setModel(currentModel);
}
}, [currentModel]);
React.useEffect(() => {
setMessages(messageBuffer.getMessages());
const unsubscribe = messageBuffer.onUpdate((newMessages) => {
setMessages(newMessages);
const modelMessage = [...newMessages].reverse().find(
(msg) => msg.type === "system" && msg.content.startsWith("[MODEL:")
);
if (modelMessage) {
const modelMatch = modelMessage.content.match(/\[MODEL:(.+?)\]/);
if (modelMatch && modelMatch[1]) {
const extractedModel = modelMatch[1];
setModel((prevModel) => {
if (extractedModel !== prevModel) {
return extractedModel;
}
return prevModel;
});
}
}
});
return () => {
unsubscribe();
if (confirmationTimeoutRef.current) {
clearTimeout(confirmationTimeoutRef.current);
}
};
}, [messageBuffer]);
const resetConfirmation = React.useCallback(() => {
setConfirmationMode(false);
if (confirmationTimeoutRef.current) {
clearTimeout(confirmationTimeoutRef.current);
confirmationTimeoutRef.current = null;
}
}, []);
const setConfirmationWithTimeout = React.useCallback(() => {
setConfirmationMode(true);
if (confirmationTimeoutRef.current) {
clearTimeout(confirmationTimeoutRef.current);
}
confirmationTimeoutRef.current = setTimeout(() => {
resetConfirmation();
}, 15e3);
}, [resetConfirmation]);
ink.useInput(React.useCallback(async (input, key) => {
if (actionInProgress) return;
if (key.ctrl && input === "c") {
if (confirmationMode) {
resetConfirmation();
setActionInProgress(true);
await new Promise((resolve) => setTimeout(resolve, 100));
onExit?.();
} else {
setConfirmationWithTimeout();
}
return;
}
if (confirmationMode) {
resetConfirmation();
}
}, [confirmationMode, actionInProgress, onExit, setConfirmationWithTimeout, resetConfirmation]));
const getMessageColor = (type) => {
switch (type) {
case "user":
return "magenta";
case "assistant":
return "cyan";
case "system":
return "blue";
case "tool":
return "yellow";
case "result":
return "green";
case "status":
return "gray";
default:
return "white";
}
};
const formatMessage = (msg) => {
const lines = msg.content.split("\n");
const maxLineLength = terminalWidth - 10;
return lines.map((line) => {
if (line.length <= maxLineLength) return line;
const chunks = [];
for (let i = 0; i < line.length; i += maxLineLength) {
chunks.push(line.slice(i, i + maxLineLength));
}
return chunks.join("\n");
}).join("\n");
};
return /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", width: terminalWidth, height: terminalHeight }, /* @__PURE__ */ React.createElement(
ink.Box,
{
flexDirection: "column",
width: terminalWidth,
height: terminalHeight - 4,
borderStyle: "round",
borderColor: "gray",
paddingX: 1,
overflow: "hidden"
},
/* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React.createElement(ink.Text, { color: "cyan", bold: true }, "\u2728 Gemini Agent Messages"), /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", dimColor: true }, "\u2500".repeat(Math.min(terminalWidth - 4, 60)))),
/* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", height: terminalHeight - 10, overflow: "hidden" }, messages.length === 0 ? /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", dimColor: true }, "Waiting for messages...") : messages.filter((msg) => {
if (msg.type === "system" && !msg.content.trim()) {
return false;
}
if (msg.type === "system" && msg.content.startsWith("[MODEL:")) {
return false;
}
if (msg.type === "system" && msg.content.startsWith("Using model:")) {
return false;
}
return true;
}).slice(-Math.max(1, terminalHeight - 10)).map((msg, index, array) => /* @__PURE__ */ React.createElement(ink.Box, { key: msg.id, flexDirection: "column", marginBottom: index < array.length - 1 ? 1 : 0 }, /* @__PURE__ */ React.createElement(ink.Text, { color: getMessageColor(msg.type), dimColor: true }, formatMessage(msg)))))
), /* @__PURE__ */ React.createElement(
ink.Box,
{
width: terminalWidth,
borderStyle: "round",
borderColor: actionInProgress ? "gray" : confirmationMode ? "red" : "cyan",
paddingX: 2,
justifyContent: "center",
alignItems: "center",
flexDirection: "column"
},
/* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", alignItems: "center" }, actionInProgress ? /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", bold: true }, "Exiting agent...") : confirmationMode ? /* @__PURE__ */ React.createElement(ink.Text, { color: "red", bold: true }, "\u26A0\uFE0F Press Ctrl-C again to exit the agent") : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(ink.Text, { color: "cyan", bold: true }, "\u2728 Gemini Agent Running \u2022 Ctrl-C to exit"), model && /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", dimColor: true }, "Model: ", model)), process.env.DEBUG && logPath && /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", dimColor: true }, "Debug logs: ", logPath))
));
};
exports.GeminiDisplay = GeminiDisplay;
exports.createRunnerAttachments = createRunnerAttachments;