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:
+47
-5
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user