Files
claude-code-conduit/bin/ccc-queue.mjs
mikael-lovqvists-claude-agent e8bbcb293f Add ccc-server/client/queue bins and blessed queue TUI
- bin/ccc-server, ccc-client, ccc-queue wired up via package.json bin
- client/config.mjs: shared secrets/user resolution from CLI args or
  CCC_SECRETS/CCC_USER env vars
- ccc-queue: blessed TUI with two-pane layout (list + detail), polls
  every 2s, y/n to approve/deny selected item, r to refresh, q to quit

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 20:49:12 +00:00

165 lines
4.7 KiB
JavaScript
Executable File

#!/usr/bin/env node
import blessed from 'blessed';
import { load_client_config } from '../client/config.mjs';
import { get_queue, resolve_queue_item } from '../client/index.mjs';
const POLL_INTERVAL = 2000;
const { username, secret } = load_client_config(process.argv);
const screen = blessed.screen({
smartCSR: true,
title: 'ccc-queue',
});
// ── Layout ────────────────────────────────────────────────────────────────────
const list_box = blessed.list({
top: 0,
left: 0,
width: '40%',
height: '100%-3',
border: { type: 'line' },
label: ' Pending Actions ',
scrollable: true,
keys: true,
vi: true,
mouse: true,
style: {
selected: { bg: 'blue', fg: 'white', bold: true },
border: { fg: 'cyan' },
label: { fg: 'cyan', bold: true },
},
});
const detail_box = blessed.box({
top: 0,
left: '40%',
width: '60%',
height: '100%-3',
border: { type: 'line' },
label: ' Details ',
scrollable: true,
alwaysScroll: true,
tags: true,
style: {
border: { fg: 'cyan' },
label: { fg: 'cyan', bold: true },
},
});
const status_bar = blessed.box({
bottom: 0,
left: 0,
width: '100%',
height: 3,
border: { type: 'line' },
tags: true,
style: { border: { fg: 'grey' } },
});
screen.append(list_box);
screen.append(detail_box);
screen.append(status_bar);
// ── State ─────────────────────────────────────────────────────────────────────
let items = [];
let status_text = '';
function set_status(text, is_error = false) {
status_text = text;
const color = is_error ? 'red' : 'green';
status_bar.setContent(
` {${color}-fg}${text}{/${color}-fg} ` +
`{grey-fg}[y]{/grey-fg} approve ` +
`{grey-fg}[n]{/grey-fg} deny ` +
`{grey-fg}[r]{/grey-fg} refresh ` +
`{grey-fg}[q]{/grey-fg} quit`
);
screen.render();
}
function render_detail(item) {
if (!item) {
detail_box.setContent('{grey-fg}No item selected{/grey-fg}');
return;
}
const lines = [
`{bold}Action:{/bold} ${item.action}`,
`{bold}ID:{/bold} ${item.id}`,
`{bold}Submitted by:{/bold} ${item.submitted_by}`,
`{bold}Created:{/bold} ${item.created_at}`,
'',
'{bold}Params:{/bold}',
];
for (const [k, v] of Object.entries(item.params ?? {})) {
lines.push(` {cyan-fg}${k}{/cyan-fg}: ${v}`);
}
detail_box.setContent(lines.join('\n'));
}
function render_list() {
const selected = list_box.selected ?? 0;
list_box.clearItems();
if (items.length === 0) {
list_box.addItem('{grey-fg}(no pending items){/grey-fg}');
} else {
for (const item of items) {
list_box.addItem(`{yellow-fg}${item.id.slice(0, 8)}{/yellow-fg} ${item.action}`);
}
list_box.select(Math.min(selected, items.length - 1));
}
render_detail(items[list_box.selected] ?? null);
screen.render();
}
// ── Polling ───────────────────────────────────────────────────────────────────
async function refresh() {
try {
items = await get_queue(username, secret);
render_list();
set_status(`Last refresh: ${new Date().toLocaleTimeString()}`);
} catch (err) {
set_status(`Refresh failed: ${err.message}`, true);
}
}
// ── Actions ───────────────────────────────────────────────────────────────────
async function decide(decision) {
const item = items[list_box.selected];
if (!item) {
return;
}
set_status(`Sending ${decision} for ${item.id.slice(0, 8)}`);
try {
await resolve_queue_item(item.id, decision, username, secret);
await refresh();
} catch (err) {
set_status(`Failed: ${err.message}`, true);
}
}
// ── Keys ──────────────────────────────────────────────────────────────────────
screen.key(['q', 'C-c'], () => process.exit(0));
screen.key('r', refresh);
screen.key('y', () => decide('approve'));
screen.key('n', () => decide('deny'));
list_box.on('select item', () => {
render_detail(items[list_box.selected] ?? null);
screen.render();
});
list_box.focus();
// ── Boot ──────────────────────────────────────────────────────────────────────
set_status('Connecting…');
refresh();
const poll = setInterval(refresh, POLL_INTERVAL);
poll.unref();