feat: session status at a glance, file & image drop, live Ops Alerts (0.4.0)
- devdesk-state-hook (Claude UserPromptSubmit/PostToolUse/Notification/Stop/ SessionStart/SessionEnd) records whether each session is working, waiting on you or idle; --list carries it. The sidebar shows it, the header shows the current session's state, and a pulsing "needs you" chip jumps to sessions waiting for input. Session refresh no longer depends on the sidebar being open. - `devdesk attach --upload SESSION FILE BYTES` receives a file as base64 terminal input (raw mode, exact length, size-checked, sanitised name) into /var/lib/devdesk/uploads/<session>/. The UI uploads dropped or pasted files and pastes the path into Claude, which attaches images itself. - `devdesk alerts` / `--alerts`: firing and pending Grafana alert rules with severity, instances, summary and a link. The panel's "Ask Claude" types an investigation prompt into the current session. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -124,6 +124,73 @@ export async function fetchSessionList() {
|
||||
return parseListOutput(await runCommand([LIST_ARG]));
|
||||
}
|
||||
|
||||
// Firing/pending Grafana alerts (`devdesk attach --alerts`).
|
||||
export async function fetchAlerts() {
|
||||
return parseListOutput(await runCommand(['--alerts']));
|
||||
}
|
||||
|
||||
function toBase64(bytes) {
|
||||
let binary = '';
|
||||
for (let i = 0; i < bytes.length; i += 0x8000) {
|
||||
binary += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
|
||||
}
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
// Upload a file into the session's upload folder through ttyd: wait for the
|
||||
// server's ready line, stream the file as base64 input, read back {ok, path}.
|
||||
export async function uploadFile(session, file, onProgress) {
|
||||
const token = await fetchToken();
|
||||
const bytes = new Uint8Array(await file.arrayBuffer());
|
||||
const b64 = toBase64(bytes);
|
||||
const name = (file.name || 'pasted-image.png').slice(-100);
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = new WebSocket(wsUrl(window.location, ['--upload', session, name, String(bytes.length)]), ['tty']);
|
||||
socket.binaryType = 'arraybuffer';
|
||||
const decoder = new TextDecoder();
|
||||
let output = '';
|
||||
let sending = false;
|
||||
const timer = setTimeout(() => socket.close(), 30000 + bytes.length / 50);
|
||||
const send = async () => {
|
||||
const chunk = 48 * 1024;
|
||||
for (let i = 0; i < b64.length; i += chunk) {
|
||||
while (socket.bufferedAmount > 1 << 20) await sleep(20);
|
||||
if (socket.readyState !== WebSocket.OPEN) return;
|
||||
socket.send(encodeInput(b64.slice(i, i + chunk)));
|
||||
onProgress?.(Math.min(1, (i + chunk) / b64.length));
|
||||
}
|
||||
};
|
||||
socket.onopen = () => socket.send(encodeInit(token, 240, 24));
|
||||
socket.onmessage = (event) => {
|
||||
const { type, payload } = decodeServer(event.data);
|
||||
if (type !== Server.OUTPUT) return;
|
||||
output += decoder.decode(payload, { stream: true });
|
||||
if (!sending && output.includes('DEVDESK-UPLOAD-READY')) {
|
||||
sending = true;
|
||||
send();
|
||||
}
|
||||
};
|
||||
socket.onerror = () => {};
|
||||
socket.onclose = () => {
|
||||
clearTimeout(timer);
|
||||
const text = stripAnsi(output.replace(/^[\s\S]*DEVDESK-UPLOAD-READY/, ''));
|
||||
try {
|
||||
const result = parseListOutput(text);
|
||||
if (result.ok && result.path) {
|
||||
resolve(result.path);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// fall through to the error message
|
||||
}
|
||||
const message = text.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).pop();
|
||||
reject(new Error((message || 'upload failed').replace(/^devdesk:\s*/, '')));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Git + Gitea CI status for a session's folder (`devdesk attach --repo NAME`).
|
||||
export async function fetchRepo(session) {
|
||||
return parseListOutput(await runCommand(['--repo', session]));
|
||||
|
||||
Reference in New Issue
Block a user