feat: free-form sessions, rename/end from the panel, live Claude Context (0.2.0)
build-deb / Lint, test, package (push) Successful in 2m0s
build-deb / Notify on failure (push) Skipped

- 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:
2026-10-07 03:03:09 -04:00
co-authored by Claude Opus 5.5
parent 4b153c1960
commit 527cb1fda2
22 changed files with 703 additions and 171 deletions
+47 -5
View File
@@ -3,6 +3,7 @@ import { createTerminal, loadFonts } from './term.js';
import { TtydConnection, fetchToken, AuthExpiredError, UnauthorizedError } from './connection.js';
import { parseTitle, sessionFromSearch } from './protocol.js';
import { SessionsPanel, rememberSession } from './sessions.js';
import { renderContext } from './context.js';
import { bindAskClaude } from './widgets.js';
import * as notify from './notify.js';
@@ -47,6 +48,7 @@ const state = {
attempts: 0,
retryTimer: null,
openedAt: 0,
endingCurrent: false,
lastActivity: Date.now(),
};
@@ -88,6 +90,18 @@ function hideOverlay() {
ui.overlay.hidden = true;
}
function showContext() {
const data = sessionsPanel?.data;
const current = data?.sessions?.find((s) => s.name === state.session);
renderContext($('widget-context'), state.session, current?.context ?? null);
}
let refreshTimer = 0;
function refreshSoon(delay = 1500) {
clearTimeout(refreshTimer);
refreshTimer = setTimeout(() => sessionsPanel?.refresh(), delay);
}
function updateUrl(session) {
const url = new URL(window.location.href);
if (session) url.searchParams.set('arg', session);
@@ -103,6 +117,7 @@ function connect(session) {
state.session = session;
updateUrl(session);
sessionsPanel?.setCurrent(session);
showContext();
ui.sessionChip.textContent = session || 'picker';
hideOverlay();
setWsState('connecting', 'connecting');
@@ -132,6 +147,12 @@ const pickerAction = { label: 'Sessions…', run: () => connect(null) };
function onClosed(code) {
setWsState('disconnected', 'offline');
if (state.endingCurrent) {
// We just ended the session we were attached to: go back to the picker.
state.endingCurrent = false;
connect(null);
return;
}
// ttyd reports a non-zero exit of `devdesk attach` with an invalid close code,
// which browsers surface as 1006 — same as a dropped network. A close within a
// few seconds of opening means the session could not start (the reason is
@@ -229,6 +250,7 @@ function applyTitle() {
updateUrl(parsed.session);
rememberSession(parsed.session);
sessionsPanel.setCurrent(parsed.session);
showContext();
}
ui.sessionChip.textContent = parsed.session;
ui.paneTitle.textContent = parsed.pane;
@@ -311,7 +333,7 @@ function wireChrome() {
});
setInterval(() => {
if (!document.hidden && ui.body.classList.contains('dd-sidebar-open')) sessionsPanel.refresh();
}, 30 * 1000);
}, 15 * 1000);
}
// ---------------------------------------------------------------- boot
@@ -332,7 +354,10 @@ async function boot() {
pendingTitle = title;
if (!titleFrame) titleFrame = requestAnimationFrame(applyTitle);
});
term.onBell(() => notify.ring(state.session, ui.paneTitle.textContent));
term.onBell(() => {
notify.ring(state.session, ui.paneTitle.textContent);
refreshSoon(); // Claude finished a turn: context numbers just changed
});
let fitFrame = 0;
new ResizeObserver(() => {
@@ -347,13 +372,30 @@ async function boot() {
sessionsPanel = new SessionsPanel({
list: $('sessions-list'),
form: $('new-session-form'),
nameInput: $('new-session-name'),
select: $('project-select'),
openButton: $('project-open'),
refreshButton: $('sessions-refresh'),
pickerButton: $('open-picker'),
status: $('sessions-status'),
onOpen: (name) => connect(name),
onPicker: () => connect(null),
hooks: {
onOpen: (name) => connect(name),
onPicker: () => connect(null),
onEnding: () => { state.endingCurrent = true; },
onEnded: (name) => {
if (name !== state.session) state.endingCurrent = false;
},
onEndAborted: () => { state.endingCurrent = false; },
onRenamed: (from, to) => {
if (from !== state.session) return;
state.session = to;
updateUrl(to);
rememberSession(to);
ui.sessionChip.textContent = to;
sessionsPanel.setCurrent(to);
},
onData: () => showContext(),
},
});
bindAskClaude($('sidebar'), (text) => {
state.conn?.sendInput(text);