feat: v0.10.0 quieter Matrix, worktrees, usage pace, repo diffs, copy reply, turn timer, compact
- 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:
+124
-10
@@ -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 ]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user