feat: free-form sessions, rename/end from the panel, live Claude Context (0.2.0)
- Sessions no longer need a project folder: any name works and starts in DEVDESK_DEFAULT_DIR unless it matches a project; `devdesk new NAME [PROJECT]`. - Rename (✎) and end/forget (×) sessions from the Sessions panel, plus `devdesk rename`. The browser runs them as `devdesk attach --new|--rename|--end` through ttyd. Renames keep the running Claude and its recorded conversation: the hook, the run-wrapper and the statusline ask tmux for the current name. - Claude Context panel is live: devdesk-statusline snapshots model, context-window use, cost and edits per session, which --list reports; Claude's footer gets a one-line summary. The panel now sits under Sessions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+21
-11
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
Server, LIST_ARG, decodeServer, encodeBinaryInput, encodeCommand, encodeInit,
|
||||
encodeInput, encodeResize, parseListOutput, tokenUrl, wsUrl, FlowControl,
|
||||
encodeInput, encodeResize, parseListOutput, stripAnsi, tokenUrl, wsUrl, FlowControl,
|
||||
} from './protocol.js';
|
||||
|
||||
export class AuthExpiredError extends Error {}
|
||||
@@ -94,16 +94,17 @@ export class TtydConnection {
|
||||
}
|
||||
}
|
||||
|
||||
// Ask the server for sessions + projects: a short-lived `devdesk attach --list`.
|
||||
export async function fetchSessionList(timeoutMs = 8000) {
|
||||
// Run a short-lived `devdesk attach <args…>` and return its output as text.
|
||||
// ttyd is the only server-side endpoint, so session actions travel this way.
|
||||
export async function runCommand(args, timeoutMs = 8000) {
|
||||
const token = await fetchToken();
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = new WebSocket(wsUrl(window.location, LIST_ARG), ['tty']);
|
||||
const socket = new WebSocket(wsUrl(window.location, args), ['tty']);
|
||||
socket.binaryType = 'arraybuffer';
|
||||
const chunks = [];
|
||||
const timer = setTimeout(() => {
|
||||
socket.close();
|
||||
reject(new Error('session list timed out'));
|
||||
reject(new Error('devdesk did not answer'));
|
||||
}, timeoutMs);
|
||||
socket.onopen = () => socket.send(encodeInit(token, 240, 24));
|
||||
socket.onmessage = (event) => {
|
||||
@@ -113,16 +114,25 @@ export async function fetchSessionList(timeoutMs = 8000) {
|
||||
socket.onerror = () => {};
|
||||
socket.onclose = () => {
|
||||
clearTimeout(timer);
|
||||
try {
|
||||
const text = new TextDecoder().decode(concat(chunks));
|
||||
resolve(parseListOutput(text));
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
resolve(new TextDecoder().decode(concat(chunks)));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Sessions, projects and per-session Claude context (`devdesk attach --list`).
|
||||
export async function fetchSessionList() {
|
||||
return parseListOutput(await runCommand([LIST_ARG]));
|
||||
}
|
||||
|
||||
// `devdesk attach --new|--rename|--end …`: resolves on {"ok":true}, otherwise
|
||||
// rejects with devdesk's own error message.
|
||||
export async function sessionAction(action, ...args) {
|
||||
const output = stripAnsi(await runCommand([`--${action}`, ...args])).trim();
|
||||
if (/"ok"\s*:\s*true/.test(output)) return;
|
||||
const message = output.split(/\r?\n/).filter(Boolean).pop() || 'no response';
|
||||
throw new Error(message.replace(/^devdesk:\s*/, ''));
|
||||
}
|
||||
|
||||
function concat(chunks) {
|
||||
const total = chunks.reduce((n, c) => n + c.length, 0);
|
||||
const out = new Uint8Array(total);
|
||||
|
||||
Reference in New Issue
Block a user