feat: v0.10.0 quieter Matrix, worktrees, usage pace, repo diffs, copy reply, turn timer, compact
build-deb / Notify on failure (push) Successful in 4s
build-deb / Lint, test, package (push) Failing after 2m22s
build-deb / Deploy to CT 126 (push) Skipped

- Matrix "needs you": ignore Claude's idle reminder (no red, no post); post
  only when no tab is in view, after 60 s, once per session per 10 min, and
  say what is asked. Closes #50
- devdesk new NAME PROJECT --worktree: own checkout on devdesk/NAME,
  cleaned up on end when clean/merged. Closes #44
- Usage meters: reset time, pace warning; Matrix ping past 90% when away.
  Closes #45
- Repo panel lists changed files; click opens the diff. Closes #46
- Copy Claude's last reply (--last-reply). Closes #47
- Turn timer in chip, title and cards. Closes #48
- Compact button at 80% context when idle. Closes #49

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHRLCCdXYm41Qx5kNhehaP
This commit is contained in:
2026-10-11 23:28:11 -04:00
co-authored by Claude Opus 5.5
parent bf8821f3f9
commit a2edec966a
17 changed files with 556 additions and 78 deletions
+124 -10
View File
@@ -14,6 +14,7 @@ setup() {
export DEVDESK_SHARE="$REPO/share"
export DEVDESK_NO_SYSTEMD=1
export DEVDESK_MATRIX_WEBHOOK_FILE="$TMP/no-webhook" # never the real room
export DEVDESK_PRESENCE_DIR="$TMP/presence" # never the live tabs
export FAKE_LOG="$TMP/claude-args"
export CLAUDE_BIN="$TMP/fake-claude"
export CLAUDE_CONFIG_DIR="$TMP/claude" # never the real conversation history
@@ -30,6 +31,7 @@ EOF
}
teardown() {
[ -z "${RECEIVER:-}" ] || kill "$RECEIVER" 2>/dev/null || true
tmux -S "$DEVDESK_SOCKET" kill-server 2>/dev/null || true
rm -rf "$TMP"
}
@@ -310,21 +312,28 @@ PY
posts() { [ -f "$TMP/posts" ] && wc -l <"$TMP/posts" || echo 0; }
@test "a wait nobody is attached to is posted to Matrix once (#28)" {
@test "a real wait is posted to Matrix once, saying what Claude asks (#28, #50)" {
start_server
start_receiver
export DEVDESK_MATRIX_DELAY=1 DEVDESK_PUBLIC_URL=https://dev.example.org
devdesk new alpha
pane=$(tmux -S "$DEVDESK_SOCKET" display -p -t =alpha:shell '#{pane_id}')
hook() { TMUX="$DEVDESK_SOCKET,1,0" TMUX_PANE="$pane" "$REPO/libexec/devdesk-state-hook"; }
st="$DEVDESK_STATE_DIR/state/alpha.json"
echo '{"hook_event_name":"Notification","message":"Claude needs your permission to use Bash"}' | hook
# a second notification in the same wait: no second message
# Claude's idle reminder after a turn is not a request: no red, no post.
echo '{"hook_event_name":"Stop"}' | hook
echo '{"hook_event_name":"Notification","message":"Claude is waiting for your input","notification_type":"idle_prompt"}' | hook
echo '{"hook_event_name":"Notification","message":"Claude is waiting for your input"}' | hook
[ "$(jq -r .state "$st")" = idle ]
echo '{"hook_event_name":"PermissionRequest","tool_name":"Bash","tool_input":{"command":"systemctl restart nginx","description":"Restart nginx"}}' | hook
# a second notification in the same wait: no second message
echo '{"hook_event_name":"Notification","message":"Claude needs your permission to use Bash","notification_type":"permission_prompt"}' | hook
sleep 2.5
[ "$(posts)" -eq 1 ]
run jq -r '[.session, .url, (.text | test("Claude needs you in alpha"))] | @tsv' "$TMP/posts"
[ "$output" = "$(printf 'alpha\thttps://dev.example.org/?arg=alpha&existing=1\ttrue')" ]
run jq -r '.text' "$TMP/posts"
[ "$output" = "$(printf 'alpha: Claude wants to run Bash (Restart nginx)\nsystemctl restart nginx\nAnswer: https://dev.example.org/?arg=alpha&existing=1')" ]
# answered within the delay: nothing is posted
echo '{"hook_event_name":"PostToolUse"}' | hook
@@ -333,12 +342,18 @@ posts() { [ -f "$TMP/posts" ] && wc -l <"$TMP/posts" || echo 0; }
sleep 2
[ "$(posts)" -eq 1 ]
# no webhook file: feature off
rm "$TMP/webhook"
echo '{"hook_event_name":"Notification","message":"again"}' | hook
sleep 2
# another real wait soon after: quiet period, still one message
echo '{"hook_event_name":"Notification","message":"Claude needs your permission to use Edit"}' | hook
sleep 2.5
[ "$(posts)" -eq 1 ]
echo '{"hook_event_name":"PostToolUse"}' | hook
# past the quiet period, but a DevDesk tab is in view: nothing
rm -rf "$DEVDESK_STATE_DIR/matrix-sent"
mkdir -p "$DEVDESK_PRESENCE_DIR" && touch "$DEVDESK_PRESENCE_DIR/1"
echo '{"hook_event_name":"Notification","message":"Claude needs your permission to use Edit"}' | hook
sleep 2.5
[ "$(posts)" -eq 1 ]
kill "$RECEIVER"
}
@test "under ttyd, one-shot replies wait for the browser to hang up (#35)" {
@@ -1068,3 +1083,102 @@ fake_grafana() {
run devdesk attach --investigate 'bad name' u-ok
[[ $output == *"invalid session name"* ]]
}
@test "last-reply returns every text part of the answer to your last message (#47)" {
start_server
devdesk new alpha
export CLAUDE_CONFIG_DIR="$TMP/claude"
id=11111111-2222-3333-4444-555555555555
mkdir -p "$CLAUDE_CONFIG_DIR/projects/-x"
cat >"$CLAUDE_CONFIG_DIR/projects/-x/$id.jsonl" <<'JSONL'
{"type":"user","message":{"role":"user","content":"First question"},"timestamp":"2026-10-10T09:00:00Z"}
{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"Old answer"}]},"timestamp":"2026-10-10T09:00:01Z"}
{"type":"user","message":{"role":"user","content":"Please upgrade PBS"},"timestamp":"2026-10-10T10:00:00Z"}
{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"Checking the version."}]},"timestamp":"2026-10-10T10:00:04Z"}
{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","name":"Bash","input":{}}]},"timestamp":"2026-10-10T10:00:05Z"}
{"type":"user","message":{"role":"user","content":[{"type":"tool_result","content":"ok"}]},"timestamp":"2026-10-10T10:00:06Z"}
{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"## Done\n\nPBS is on **4.1**."}]},"timestamp":"2026-10-10T10:00:09Z"}
JSONL
run devdesk attach --last-reply alpha
[ "$status" -ne 0 ] # no conversation recorded yet
jq --arg id "$id" '.sessions.alpha.claude_session_id = $id' "$DEVDESK_STATE_DIR/registry.json" >"$TMP/r" && mv "$TMP/r" "$DEVDESK_STATE_DIR/registry.json"
run devdesk attach --last-reply alpha
[ "$status" -eq 0 ]
[ "$(jq -r .text <<<"$output")" = "$(printf 'Checking the version.\n\n## Done\n\nPBS is on **4.1**.')" ]
[ "$(jq -r .at <<<"$output")" = 2026-10-10T10:00:09Z ]
}
@test "worktree sessions get their own checkout; ending removes it only when clean (#44)" {
start_server
repo="$DEVDESK_PROJECTS_ROOT/alpha"
git -C "$repo" init -q -b main
git -C "$repo" -c user.name=t -c user.email=t@t commit -q --allow-empty -m init
mkdir -p "$DEVDESK_PROJECTS_ROOT/plain"
run devdesk new solo --worktree
[ "$status" -ne 0 ]
run devdesk new p1 plain --worktree
[ "$status" -ne 0 ]
[[ $output == *"not a git repository"* ]]
devdesk new wt1 alpha --worktree
wt="$DEVDESK_PROJECTS_ROOT/.worktrees/alpha--wt1"
[ "$(git -C "$wt" symbolic-ref --short HEAD)" = devdesk/wt1 ]
run tmux -S "$DEVDESK_SOCKET" display -p -t =wt1:claude '#{pane_current_path}'
[ "$output" = "$wt" ]
run devdesk attach --list
[ "$(jq -r '.sessions[] | select(.name=="wt1") | .worktree.branch' <<<"$output")" = devdesk/wt1 ]
[ "$(jq -r '.projects[] | select(.name=="alpha") | .git' <<<"$output")" = true ]
[ "$(jq -r '.projects[] | select(.name=="plain") | .git' <<<"$output")" = false ]
[ "$(jq -r '[.projects[].name] | index(".worktrees")' <<<"$output")" = null ]
# clean and merged: worktree and branch go
devdesk end wt1
[ ! -e "$wt" ]
run git -C "$repo" show-ref --verify refs/heads/devdesk/wt1
[ "$status" -ne 0 ]
# uncommitted work: everything stays
devdesk new wt2 alpha --worktree
wt="$DEVDESK_PROJECTS_ROOT/.worktrees/alpha--wt2"
echo work >"$wt/file"
devdesk end wt2
[ -f "$wt/file" ]
git -C "$repo" show-ref -q --verify refs/heads/devdesk/wt2
}
@test "repo lists the first changed files for the panel (#46)" {
export DEVDESK_GITEA_URL=http://127.0.0.1:9
repo="$DEVDESK_PROJECTS_ROOT/alpha"
git -C "$repo" init -q -b main
printf 'a\n' >"$repo/tracked"
git -C "$repo" add tracked
git -C "$repo" -c user.name=t -c user.email=t@t commit -q -m init
printf 'a\nb\n' >"$repo/tracked"
touch "$repo/new-file"
run devdesk attach --repo alpha
[ "$status" -eq 0 ]
[ "$(jq -c '[.changes[] | [.path, .x, .y, .add]]' <<<"$output")" = '[["tracked"," ","M",1],["new-file","?","?",null]]' ]
}
@test "plan usage past the threshold pings Matrix once per window, only when away (#45)" {
start_server
start_receiver
devdesk new alpha
pane=$(tmux -S "$DEVDESK_SOCKET" display -p -t =alpha:shell '#{pane_id}')
sl() { echo "{\"model\":{\"display_name\":\"Opus\"},\"context_window\":{},\"rate_limits\":{\"five_hour\":{\"used_percentage\":$1,\"resets_at\":$2}}}" |
TMUX="$DEVDESK_SOCKET,1,0" TMUX_PANE="$pane" "$REPO/libexec/devdesk-statusline" >/dev/null; }
sl 50 2000000000
sl 91 2000000000
sl 93 2000000000
for _ in $(seq 1 30); do [ "$(posts)" -ge 1 ] && break; sleep 0.1; done
sleep 0.5
[ "$(posts)" -eq 1 ]
[[ "$(jq -r .text "$TMP/posts")" == *"91% of the 5-hour window"* ]]
# next window, but a client is attached: no post, and that window is done
tmux -S "$DEVDESK_SOCKET" new-session -d -s watcher 'sleep 30'
script -qc "tmux -S '$DEVDESK_SOCKET' attach -t =alpha" /dev/null </dev/null >/dev/null 2>&1 &
for _ in $(seq 1 30); do [ -n "$(tmux -S "$DEVDESK_SOCKET" list-clients -F x)" ] && break; sleep 0.1; done
sl 95 2000018000
sleep 1
[ "$(posts)" -eq 1 ]
[ "$(cat "$DEVDESK_STATE_DIR/usage-warned")" = 2000018000 ]
}