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:
+23
-5
@@ -57,6 +57,20 @@ export function el(tag, attrs = {}, ...children) {
|
||||
return node;
|
||||
}
|
||||
|
||||
// What Claude is doing in a session, from the devdesk-state-hook snapshot.
|
||||
// Returns { key, label, dot } for display; key is one of
|
||||
// waiting | working | idle | exited | running | dormant.
|
||||
export function sessionStatus(s) {
|
||||
if (!s?.running) return { key: 'dormant', label: 'dormant', dot: 'lt-dot-idle' };
|
||||
switch (s.status?.state) {
|
||||
case 'waiting': return { key: 'waiting', label: 'needs you', dot: 'lt-dot-down' };
|
||||
case 'working': return { key: 'working', label: 'working', dot: 'lt-dot-warn' };
|
||||
case 'idle': return { key: 'idle', label: 'idle', dot: 'lt-dot-up' };
|
||||
case 'exited': return { key: 'exited', label: 'Claude exited', dot: 'lt-dot-idle' };
|
||||
default: return { key: 'running', label: 'running', dot: 'lt-dot-up' };
|
||||
}
|
||||
}
|
||||
|
||||
export class SessionsPanel {
|
||||
// hooks: onOpen(name), onPicker(), onEnded(name), onRenamed(from, to), onData(data)
|
||||
constructor({ list, form, nameInput, select, refreshButton, pickerButton, status, hooks }) {
|
||||
@@ -156,15 +170,19 @@ export class SessionsPanel {
|
||||
|
||||
renderSession(s) {
|
||||
const isCurrent = s.name === this.current;
|
||||
const ctx = s.context?.used_pct != null ? ` · ctx ${Math.floor(s.context.used_pct)}%` : '';
|
||||
const status = sessionStatus(s);
|
||||
const ctx = s.context?.used_pct != null ? `ctx ${Math.floor(s.context.used_pct)}%` : '';
|
||||
const since = s.status?.since ? ago(s.status.since) : ago(s.activity);
|
||||
const meta = s.running
|
||||
? `${s.attached} attached · ${ago(s.activity)}${ctx}`
|
||||
? [`${status.label}${since && since !== 'now' ? ` ${since}` : ''}`, ctx, s.attached ? `${s.attached} open` : '']
|
||||
.filter(Boolean).join(' · ')
|
||||
: 'dormant · resumes Claude';
|
||||
const title = [s.dir, status.key === 'waiting' && s.status?.message].filter(Boolean).join('\n');
|
||||
const iconButton = (label, glyph, onclick, extra = '') =>
|
||||
el('button', { class: `dd-icon-btn ${extra}`, type: 'button', title: label, 'aria-label': `${label} ${s.name}`, onclick }, glyph);
|
||||
return el('li', { class: `dd-session-item${isCurrent ? ' is-current' : ''}${s.running ? '' : ' is-dormant'}` },
|
||||
el('button', { class: 'dd-session-open', type: 'button', title: s.dir || '', onclick: () => this.hooks.onOpen(s.name) },
|
||||
el('span', { class: `lt-dot ${s.running ? 'lt-dot-up' : 'lt-dot-idle'}` }),
|
||||
return el('li', { class: `dd-session-item is-${status.key}${isCurrent ? ' is-current' : ''}` },
|
||||
el('button', { class: 'dd-session-open', type: 'button', title, onclick: () => this.hooks.onOpen(s.name) },
|
||||
el('span', { class: `lt-dot ${status.dot}` }),
|
||||
el('span', { class: 'dd-session-name' }, s.name),
|
||||
el('span', { class: 'dd-session-meta' }, meta)),
|
||||
el('span', { class: 'dd-session-actions' },
|
||||
|
||||
Reference in New Issue
Block a user