Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf8821f3f9 | ||
|
|
fe4df10210 | ||
|
|
963d2f5413 |
@@ -56,7 +56,7 @@ The tmux server runs in its own unit, so every Claude process lives in **that**
|
||||
- **Search past conversations** (⋯ menu, palette): full text over every Claude conversation on the host; **Resume** it in a new session or **Fork** it.
|
||||
- **Plan usage:** the 5-hour and weekly windows as meters in the footer (warning at 90 %); a **PR** chip on sessions whose branch has a pull request.
|
||||
- **Quiet phone at the desk:** while a tab shows a session, Remote Control holds back phone pushes for it (client-presence marker, expires ~2 min after you leave).
|
||||
- **Context:** a Claude `statusLine` command (`devdesk-statusline`) snapshots each session's model, context-window use and cost to `/var/lib/devdesk/context/<session>.json`. The Sessions panel shows it as a bar (cost in the tooltip), and Claude's own footer shows a one-line summary.
|
||||
- **Context:** a Claude `statusLine` command (`devdesk-statusline`) snapshots each session's model, context-window use, plan usage and cost to `/var/lib/devdesk/context/<session>.json`. The Sessions panel shows it as a bar and Claude's own footer shows a one-line summary. On a Pro/Max plan that summary shows 5-hour plan usage; the dollar figure Claude reports there is an API-price estimate, not a bill, so it appears only for API-key billing.
|
||||
- **Light mode:** the terminal switches palettes with the page. DevDesk starts Claude with its `dark-ansi` theme so Claude draws with the terminal's 16 colours, and the light palette maps bright white to ink; tmux and the status line use ANSI colours too. A session picks up Claude's theme the next time Claude starts in it (`/exit`, then ⏎ resumes the same conversation).
|
||||
- **Phone:** a key bar replaces the shortcut hints (Esc, Tab, ⇧Tab, ^C, ↑, ↓, ⏎, Claude/Shell window), and the panels open as a drawer.
|
||||
- **SSH:** `devdesk attach <name>`, `devdesk new <name> [project]`, `devdesk rename <old> <new>`, `devdesk end <name>`, `devdesk ls`, `devdesk pick`, `devdesk repo <name>`, `devdesk alerts`.
|
||||
|
||||
Vendored
+32
@@ -1,3 +1,35 @@
|
||||
devdesk (0.9.5) bookworm; urgency=medium
|
||||
|
||||
* Context use is shown in tokens as well as percent (#43): Claude's
|
||||
footer shows "ctx 34% 346k/1M", and the Sessions panel and session
|
||||
switcher show "ctx 34% · 346k/1M".
|
||||
|
||||
-- Jared Vititoe <jjvititoe1@gmail.com> Sun, 11 Oct 2026 23:07:00 -0400
|
||||
|
||||
devdesk (0.9.4) bookworm; urgency=medium
|
||||
|
||||
* The inbox badge no longer only grows (#42). Opening the bell marks its
|
||||
events seen, leaving just what is live (sessions waiting on you,
|
||||
firing alerts). Viewing a session reads the events it reported while
|
||||
you were away.
|
||||
|
||||
-- Jared Vititoe <jjvititoe1@gmail.com> Sun, 11 Oct 2026 22:46:00 -0400
|
||||
|
||||
devdesk (0.9.3) bookworm; urgency=medium
|
||||
|
||||
* Claude's footer shows 5-hour plan use instead of a dollar figure on a
|
||||
Pro/Max subscription (#40). Claude Code's cost is an API-price
|
||||
estimate, not a bill; it is still shown for API-key billing. The
|
||||
Sessions tooltip follows the same rule.
|
||||
* The bottom row is larger and easier to read (#41): 40px tall, larger
|
||||
hints, and 5h/week usage meters in bold monospace with wider bars.
|
||||
Narrow windows drop the least-used hints instead of clipping the
|
||||
meters.
|
||||
* Tests clear inherited DevDesk variables, so they pass when run from
|
||||
inside a DevDesk session.
|
||||
|
||||
-- Jared Vititoe <jjvititoe1@gmail.com> Sun, 11 Oct 2026 22:08:00 -0400
|
||||
|
||||
devdesk (0.9.2) bookworm; urgency=medium
|
||||
|
||||
* Ops Alerts → Ask Claude starts an investigation (#39):
|
||||
|
||||
@@ -43,13 +43,21 @@ if [[ $session =~ ^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$ ]]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# Footer line: session · model · context % · cost. ANSI colour numbers, not RGB,
|
||||
# so the browser's dark or light palette applies (devdesk#31).
|
||||
# Footer line: session · model · context % and tokens · plan use. ANSI colour numbers, not
|
||||
# RGB, so the browser's dark or light palette applies (devdesk#31). Claude sends
|
||||
# rate_limits only to Pro/Max subscribers; there .cost is an API-price estimate,
|
||||
# not a bill, so show the 5-hour plan use instead and keep $ for API-key billing.
|
||||
jq -rM --arg s "$session" '
|
||||
def c($sgr; $t): "\u001b[\($sgr)m\($t)\u001b[0m";
|
||||
def pct: if . == null then "—" else "\(. | floor)%" end;
|
||||
def tok: if . >= 1000000 then "\(. / 100000 | round / 10)M" elif . >= 1000 then "\(. / 1000 | round)k" else "\(.)" end;
|
||||
def ctxtok: .context_window | (.current_usage | if . then (.input_tokens // 0) + (.cache_creation_input_tokens // 0)
|
||||
+ (.cache_read_input_tokens // 0) else null end) as $u
|
||||
| if $u == null then "" else " \($u | tok)\(if .context_window_size then "/\(.context_window_size | tok)" else "" end)" end;
|
||||
[ (if $s != "" then c("33"; "◈ \($s)") else empty end),
|
||||
c("36"; .model.display_name // "Claude"),
|
||||
c("37"; "ctx \(.context_window.used_percentage | pct)"),
|
||||
c("37"; "$\((.cost.total_cost_usd // 0) * 100 | round / 100)")
|
||||
c("37"; "ctx \(.context_window.used_percentage | pct)\(ctxtok)"),
|
||||
(if .rate_limits.five_hour.used_percentage != null
|
||||
then c("37"; "5h \(.rate_limits.five_hour.used_percentage | pct)")
|
||||
else c("37"; "$\((.cost.total_cost_usd // 0) * 100 | round / 100)") end)
|
||||
] | join(c("90"; " · "))' <<<"$input" 2>/dev/null || true
|
||||
|
||||
+13
-1
@@ -5,6 +5,7 @@ REPO=$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)
|
||||
|
||||
setup() {
|
||||
TMP=$(mktemp -d /tmp/devdesk-test.XXXXXX) # short path: unix socket limit
|
||||
unset DEVDESK_REMOTE_CONTROL CLAUDE_ARGS DEVDESK_SESSION TMUX TMUX_PANE # when run from inside DevDesk
|
||||
export DEVDESK_ENV=/dev/null
|
||||
export DEVDESK_PROJECTS_ROOT="$TMP/projects"
|
||||
export DEVDESK_STATE_DIR="$TMP/state"
|
||||
@@ -194,12 +195,23 @@ claude_args() {
|
||||
"cost":{"total_cost_usd":1.5,"total_lines_added":10,"total_lines_removed":2}}'
|
||||
run bash -c "echo '$status_json' | TMUX='$DEVDESK_SOCKET,1,0' TMUX_PANE='$pane' '$REPO/libexec/devdesk-statusline'"
|
||||
[ "$status" -eq 0 ]
|
||||
[[ $output == *"alpha"* && $output == *"ctx 34%"* ]]
|
||||
[[ $output == *"alpha"* && $output == *"ctx 34% 346k/1M"* ]]
|
||||
run devdesk attach --list
|
||||
[ "$(jq -r '.sessions[] | select(.name=="alpha") | .context.used_tokens' <<<"$output")" = 346000 ]
|
||||
[ "$(jq -r '.sessions[] | select(.name=="alpha") | .context.model' <<<"$output")" = Opus ]
|
||||
}
|
||||
|
||||
@test "statusline footer shows plan use on a subscription and dollars only on API billing" {
|
||||
sub='{"model":{"display_name":"Opus"},"context_window":{"used_percentage":10},
|
||||
"cost":{"total_cost_usd":1186.99},"rate_limits":{"five_hour":{"used_percentage":8.4}}}'
|
||||
run bash -c "echo '$sub' | '$REPO/libexec/devdesk-statusline'"
|
||||
[[ $output == *"5h 8%"* ]]
|
||||
[[ $output != *'$'* ]]
|
||||
api='{"model":{"display_name":"Opus"},"context_window":{"used_percentage":10},"cost":{"total_cost_usd":1.5}}'
|
||||
run bash -c "echo '$api' | '$REPO/libexec/devdesk-statusline'"
|
||||
[[ $output == *'$1.5'* ]]
|
||||
}
|
||||
|
||||
@test "hooks ignore Claude running in some other tmux server" {
|
||||
start_server
|
||||
other="$TMP/other.sock"
|
||||
|
||||
+26
-2
@@ -69,7 +69,27 @@ export class Inbox {
|
||||
this.button.setAttribute('aria-expanded', String(open));
|
||||
if (!open) this.peeking = null;
|
||||
this.panel.classList.toggle('is-peek', false);
|
||||
if (open) this.render();
|
||||
if (open) {
|
||||
this.render();
|
||||
this.markSeen();
|
||||
}
|
||||
}
|
||||
|
||||
// Opening the bell counts as seeing what's in it (devdesk#42): the badge clears
|
||||
// but the entries keep their highlight until the panel is redrawn.
|
||||
markSeen() {
|
||||
if (!this.events.some((e) => !e.read)) return;
|
||||
this.events = this.events.map((e) => ({ ...e, read: true }));
|
||||
save(this.events);
|
||||
this.renderBadge();
|
||||
}
|
||||
|
||||
// Looking at a session reads what it reported (finished, waiting) while you were away.
|
||||
markSessionRead(session) {
|
||||
if (!session || !this.events.some((e) => !e.read && e.session === session && !e.url)) return;
|
||||
this.events = this.events.map((e) => (e.session === session && !e.url ? { ...e, read: true } : e));
|
||||
save(this.events);
|
||||
this.render();
|
||||
}
|
||||
|
||||
// Something happened. The same kind for the same session within a minute is
|
||||
@@ -102,6 +122,11 @@ export class Inbox {
|
||||
}
|
||||
|
||||
render() {
|
||||
this.renderBadge();
|
||||
if (this.isOpen) this.renderPanel();
|
||||
}
|
||||
|
||||
renderBadge() {
|
||||
const count = this.waiting.length + this.alerts.length + this.unread();
|
||||
const urgent = this.waiting.length > 0 || this.alerts.some((a) => a.severity === 'critical');
|
||||
this.badge.textContent = count ? String(count) : '';
|
||||
@@ -114,7 +139,6 @@ export class Inbox {
|
||||
this.unread() && `${this.unread()} new event(s)`,
|
||||
].filter(Boolean).join(' · ')
|
||||
: 'Inbox: nothing new';
|
||||
if (this.isOpen) this.renderPanel();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ answer from here
|
||||
|
||||
+7
-3
@@ -5,7 +5,7 @@ import {
|
||||
TtydConnection, fetchAlerts, fetchRepo, fetchToken, investigateAlert, sessionAction, uploadFile, AuthExpiredError, UnauthorizedError,
|
||||
} from './connection.js';
|
||||
import { parseTitle, pasteSafe, sessionFromSearch } from './protocol.js';
|
||||
import { SessionsPanel, recentNames, rememberSession, sessionStatus } from './sessions.js';
|
||||
import { SessionsPanel, ctxLabel, recentNames, rememberSession, sessionStatus } from './sessions.js';
|
||||
import { renderAlerts } from './alerts.js';
|
||||
import { renderRepo } from './repo.js';
|
||||
import { bindAskClaude } from './widgets.js';
|
||||
@@ -181,6 +181,7 @@ function renderPanelsBadge() {
|
||||
}
|
||||
|
||||
function onSessionData({ fetched = false } = {}) {
|
||||
if (document.visibilityState === 'visible') inbox?.markSessionRead(state.session);
|
||||
renderUsage();
|
||||
renderSessionChip();
|
||||
renderAttention(fetched);
|
||||
@@ -546,7 +547,7 @@ function renderSwitcher() {
|
||||
};
|
||||
const items = sessions.map((s) => {
|
||||
const status = sessionStatus(s);
|
||||
const ctx = s.context?.used_pct != null ? ` · ctx ${Math.floor(s.context.used_pct)}%` : '';
|
||||
const ctx = s.context?.used_pct != null ? ` · ${ctxLabel(s.context)}` : '';
|
||||
return el('button', {
|
||||
type: 'button', role: 'menuitem',
|
||||
class: `dd-menu-item dd-switch-item is-${status.key}${s.name === state.session ? ' is-current' : ''}`,
|
||||
@@ -1074,7 +1075,10 @@ function wireChrome() {
|
||||
|
||||
window.addEventListener('online', retrySoon);
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (!document.hidden) retrySoon();
|
||||
if (!document.hidden) {
|
||||
retrySoon();
|
||||
inbox.markSessionRead(state.session);
|
||||
}
|
||||
});
|
||||
// Session states feed the header chip and the notifications for other sessions,
|
||||
// so poll even in a background tab (browsers throttle that to about once a
|
||||
|
||||
+11
-2
@@ -46,6 +46,14 @@ function renameRecent(from, to) {
|
||||
writeRecents(readRecents().map((r) => (r.name === from ? { ...r, name: to } : r)));
|
||||
}
|
||||
|
||||
// "ctx 34% · 346k/1M": context use as a share of the window and in tokens.
|
||||
const tokens = (n) => (n >= 1e6 ? `${+(n / 1e6).toFixed(n % 1e6 ? 1 : 0)}M` : n >= 1000 ? `${Math.round(n / 1000)}k` : String(n));
|
||||
export function ctxLabel(c) {
|
||||
if (c?.used_pct == null) return '';
|
||||
const count = c.used_tokens != null ? ` · ${tokens(c.used_tokens)}${c.window ? `/${tokens(c.window)}` : ''}` : '';
|
||||
return `ctx ${Math.floor(c.used_pct)}%${count}`;
|
||||
}
|
||||
|
||||
export function ago(epochSeconds) {
|
||||
if (!epochSeconds) return '';
|
||||
const s = Math.max(0, Date.now() / 1000 - epochSeconds);
|
||||
@@ -267,7 +275,7 @@ export class SessionsPanel {
|
||||
renderSession(s) {
|
||||
const isCurrent = s.name === this.current;
|
||||
const status = sessionStatus(s);
|
||||
const ctx = s.context?.used_pct != null ? `ctx ${Math.floor(s.context.used_pct)}%` : '';
|
||||
const ctx = ctxLabel(s.context);
|
||||
const since = s.status?.since ? ago(s.status.since) : ago(s.activity);
|
||||
const model = isCurrent && s.context?.model ? s.context.model.replace(/^Claude\s+/i, '') : '';
|
||||
const meta = s.running
|
||||
@@ -278,7 +286,8 @@ export class SessionsPanel {
|
||||
const title = [
|
||||
s.dir,
|
||||
status.key === 'waiting' && s.status?.message,
|
||||
s.context?.model && `${s.context.model}${s.context.cost_usd != null ? ` · $${s.context.cost_usd.toFixed(2)}` : ''}`,
|
||||
// rate_limits means a Pro/Max plan, where cost_usd is only an API-price estimate
|
||||
s.context?.model && `${s.context.model}${s.context.rate_limits ? '' : s.context.cost_usd != null ? ` · $${s.context.cost_usd.toFixed(2)}` : ''}`,
|
||||
].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);
|
||||
|
||||
+20
-4
@@ -2,7 +2,7 @@
|
||||
everything visual comes from the TDS tokens defined in base.css :root. */
|
||||
|
||||
:root {
|
||||
--dd-footer-height: 30px;
|
||||
--dd-footer-height: 40px;
|
||||
--dd-sidebar-width: 320px;
|
||||
--dd-gap: 8px;
|
||||
}
|
||||
@@ -492,7 +492,19 @@ body.dd-sidebar-open .dd-sidebar {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dd-footer .lt-footer-key { margin-right: 4px; }
|
||||
.dd-footer .lt-footer-key { margin-right: 5px; font-size: 0.86rem; }
|
||||
/* devdesk#41: the bottom row was too small to read at a glance */
|
||||
.dd-footer .lt-footer-hint { font-size: 0.9rem; }
|
||||
.dd-footer .lt-footer-sep { font-size: 0.9rem; }
|
||||
/* Narrower windows keep the usage meters and drop the least-used hints. */
|
||||
@media (max-width: 1400px) {
|
||||
.dd-footer .lt-footer-hints .lt-footer-hint:nth-child(n + 13),
|
||||
.dd-footer .lt-footer-sep:nth-child(n + 12) { display: none; }
|
||||
}
|
||||
@media (max-width: 1150px) {
|
||||
.dd-footer .lt-footer-hints .lt-footer-hint:nth-child(n + 9),
|
||||
.dd-footer .lt-footer-sep:nth-child(n + 8) { display: none; }
|
||||
}
|
||||
|
||||
.dd-keybar { display: flex; gap: 4px; width: 100%; overflow-x: auto; scrollbar-width: none; }
|
||||
.dd-keybar[hidden] { display: none; }
|
||||
@@ -625,7 +637,7 @@ html[data-theme="light"] .lt-cmd-item.is-selected { background: var(--accent-cya
|
||||
background: none;
|
||||
color: var(--accent-cyan);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.64rem;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.dd-footer-help:hover { border-color: var(--accent-cyan); }
|
||||
@@ -1005,11 +1017,15 @@ html[data-theme="light"] .dd-files.is-narrow .dd-explorer { box-shadow: 8px 0 24
|
||||
.dd-history-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 8px; }
|
||||
.dd-history-actions .lt-input { width: 220px; font-size: 0.74rem; }
|
||||
|
||||
.dd-footer-usage { margin-left: auto; padding-left: 12px; }
|
||||
.dd-footer-usage { margin-left: auto; padding-left: 16px; }
|
||||
.dd-footer-usage[hidden],
|
||||
.dd-usage-row[hidden] { display: none; }
|
||||
.dd-usage { display: inline-flex; align-items: center; gap: 10px; }
|
||||
.dd-footer-usage { display: inline-flex; align-items: center; gap: 18px; }
|
||||
.dd-meter { display: inline-flex; align-items: center; gap: 5px; color: var(--text-secondary); font-size: 0.66rem; white-space: nowrap; }
|
||||
.dd-footer-usage .dd-meter { gap: 7px; color: var(--text-primary); font-family: var(--font-mono); font-size: 1rem; font-weight: 600; }
|
||||
.dd-footer-usage .dd-meter-label { font-weight: 400; }
|
||||
.dd-footer-usage .dd-meter-bar { width: 80px; height: 8px; }
|
||||
.dd-meter-label { color: var(--text-muted); }
|
||||
.dd-meter-bar { width: 42px; height: 4px; background: var(--bg-tertiary); }
|
||||
.dd-meter-fill { display: block; height: 100%; background: var(--accent-cyan); }
|
||||
|
||||
Reference in New Issue
Block a user