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
+51
View File
@@ -0,0 +1,51 @@
// Claude Context panel: the current session's model, context-window use, cost
// and edits, from the snapshot devdesk-statusline writes on every Claude update.
import { ago } from './sessions.js';
function compact(tokens) {
if (tokens == null) return '—';
if (tokens >= 1e6) return `${+(tokens / 1e6).toFixed(tokens % 1e6 ? 1 : 0)}M`;
if (tokens >= 1e3) return `${Math.round(tokens / 1e3)}k`;
return String(tokens);
}
export function renderContext(root, session, context) {
const set = (field, text) => {
const node = root.querySelector(`[data-field="${field}"]`);
if (node) node.textContent = text;
};
const bar = root.querySelector('.lt-progress');
const fill = root.querySelector('.lt-progress-bar');
const note = root.querySelector('.dd-context-note');
if (!context) {
root.dataset.state = 'empty';
fill.style.width = '0%';
bar.setAttribute('aria-valuenow', '0');
for (const f of ['used', 'model', 'cost', 'lines', 'updated']) set(f, '—');
note.textContent = session
? 'No data yet: it appears once Claude has redrawn its status line in this session.'
: 'Open a session to see its context.';
return;
}
const pct = context.used_pct;
root.dataset.state = 'live';
fill.style.width = `${Math.min(100, pct ?? 0)}%`;
bar.setAttribute('aria-valuenow', String(Math.round(pct ?? 0)));
bar.classList.remove('lt-progress--cyan', 'lt-progress--red');
if (pct == null || pct < 70) bar.classList.add('lt-progress--cyan');
else if (pct >= 90) bar.classList.add('lt-progress--red');
set('used', pct == null
? `— / ${compact(context.window)}`
: `${compact(context.used_tokens)} / ${compact(context.window)} (${pct.toFixed(pct < 10 ? 1 : 0)}%)`);
set('model', [context.model, context.effort].filter(Boolean).join(' · ') || '—');
set('cost', context.cost_usd == null ? '—' : `$${context.cost_usd.toFixed(2)}`);
set('lines', `+${context.lines_added ?? 0} −${context.lines_removed ?? 0}`);
set('updated', context.updated ? `${ago(context.updated)} ago`.replace('now ago', 'just now') : '—');
note.textContent = pct != null && pct >= 80
? 'Context is filling up; consider /compact or a new conversation.'
: '';
}