@ramarivera/coding-buddy 0.8.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -4
- package/adapters/omp/context.ts +1 -1
- package/adapters/omp/events.ts +286 -81
- package/adapters/omp/index.ts +3 -1
- package/adapters/omp/renderers.ts +4 -54
- package/adapters/omp/storage.ts +6 -1
- package/adapters/omp/ui.ts +17 -6
- package/adapters/pi/events.ts +280 -83
- package/adapters/pi/index.ts +11 -2
- package/adapters/pi/renderers.ts +3 -47
- package/adapters/pi/storage.ts +6 -1
- package/adapters/pi/ui.ts +17 -6
- package/adapters/shared/file-storage.ts +22 -0
- package/adapters/shared/widget-layout.ts +248 -2
- package/cli/settings.ts +1 -1
- package/cli/tui.tsx +1 -1
- package/core/command-service.ts +13 -0
- package/core/engine.ts +11 -2
- package/core/identity.ts +15 -0
- package/core/model.ts +3 -0
- package/core/ports.ts +4 -0
- package/core/reactions.ts +29 -2
- package/core/render-model.ts +4 -3
- package/package.json +6 -3
- package/scripts/ci/Dockerfile.vhs +125 -0
- package/scripts/ci/check-statusline.sh +441 -14
- package/scripts/ci/check-vhs-goldens.sh +97 -0
- package/scripts/ci/fonts.conf +37 -0
- package/scripts/ci/goldens/baseline-40.png +0 -0
- package/scripts/ci/goldens/baseline-80.png +0 -0
- package/scripts/ci/goldens/cjk-80.png +0 -0
- package/scripts/ci/goldens/emoji-80.png +0 -0
- package/scripts/ci/render-statusline-pty.py +107 -0
- package/scripts/ci/render-vhs-goldens.sh +226 -0
- package/scripts/ci/statusline.tape +28 -2
- package/scripts/ci/vhs-density-fixtures.sh +27 -0
- package/scripts/ci/vhs-run-statusline.sh +28 -0
- package/scripts/ci/write-density-status.ts +86 -0
- package/scripts/gen-emoji-widths.ts +41 -17
- package/scripts/paths.sh +3 -1
- package/server/art.ts +70 -17
- package/server/hooks/buddy-comment.ts +166 -17
- package/server/hooks/file-type-react.ts +1 -1
- package/server/hooks/mood-react.ts +1 -1
- package/server/hooks/name-react.ts +1 -1
- package/server/hooks/react.ts +1 -1
- package/server/hooks/reaction-data.ts +10 -3
- package/server/index.ts +67 -27
- package/server/path.ts +3 -0
- package/server/state.ts +147 -41
- package/skills/buddy/SKILL.md +1 -1
- package/statusline/buddy-status.sh +488 -76
- package/statusline/emoji-text.data +3 -0
- package/statusline/emoji-widths.data +2 -3
- package/statusline/substatus.sh +52 -10
- package/scripts/postinstall.cjs +0 -42
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Render buddy-status.sh under a fixed PTY winsize.
|
|
3
|
+
|
|
4
|
+
buddy-status.sh probes the parent TTY via stty, so COLUMNS alone is not
|
|
5
|
+
enough when CI (or a wide agent terminal) already has a PTY. This helper
|
|
6
|
+
forks a PTY, sets TIOCSWINSZ to the requested cols, and prints the raw
|
|
7
|
+
statusline bytes to stdout.
|
|
8
|
+
|
|
9
|
+
Elapsed wall time for the child (statusline) is printed on stderr as:
|
|
10
|
+
STATUSLINE_ELAPSED_MS=<int>
|
|
11
|
+
so the golden harness can budget Claude's ~1s kill without counting
|
|
12
|
+
Python/import overhead outside the child.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import fcntl
|
|
18
|
+
import os
|
|
19
|
+
import pty
|
|
20
|
+
import select
|
|
21
|
+
import struct
|
|
22
|
+
import sys
|
|
23
|
+
import termios
|
|
24
|
+
import time
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _positive_int(env_name: str, default: int) -> int:
|
|
28
|
+
value = os.environ.get(env_name, "")
|
|
29
|
+
if value and value.isdigit():
|
|
30
|
+
parsed = int(value)
|
|
31
|
+
if parsed > 0:
|
|
32
|
+
return parsed
|
|
33
|
+
return default
|
|
34
|
+
|
|
35
|
+
def main() -> int:
|
|
36
|
+
if len(sys.argv) != 5:
|
|
37
|
+
print(
|
|
38
|
+
"usage: render-statusline-pty.py <cols> <root> <config_dir> <script>",
|
|
39
|
+
file=sys.stderr,
|
|
40
|
+
)
|
|
41
|
+
return 2
|
|
42
|
+
|
|
43
|
+
cols = int(sys.argv[1])
|
|
44
|
+
root = sys.argv[2]
|
|
45
|
+
config_dir = sys.argv[3]
|
|
46
|
+
script = sys.argv[4]
|
|
47
|
+
rows = _positive_int("BUDDY_STATUSLINE_ROWS", 30)
|
|
48
|
+
payload = os.environ.get(
|
|
49
|
+
"STATUSLINE_PAYLOAD",
|
|
50
|
+
'{"session_id":"ci","model":{"display_name":"CI"},"workspace":{"current_dir":"."}}',
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Build a tiny pipeline so buddy-status.sh can `cat` its JSON stdin.
|
|
54
|
+
cmd = (
|
|
55
|
+
"printf %s "
|
|
56
|
+
+ repr(payload)
|
|
57
|
+
+ " | "
|
|
58
|
+
+ f"COLUMNS={cols} BUDDY_FAKE_NOW=0 BUDDY_STATUSLINE_ROWS={rows} "
|
|
59
|
+
+ f"CLAUDE_CONFIG_DIR={config_dir!r} "
|
|
60
|
+
+ "CLAUDE_CODE_SESSION_ID= TMUX_PANE= BUDDY_SHELL= "
|
|
61
|
+
+ f"bash {script!r}"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
t0 = time.perf_counter()
|
|
65
|
+
pid, fd = pty.fork()
|
|
66
|
+
if pid == 0:
|
|
67
|
+
os.chdir(root)
|
|
68
|
+
os.execvp("bash", ["bash", "-c", cmd])
|
|
69
|
+
|
|
70
|
+
fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", rows, cols, 0, 0))
|
|
71
|
+
out = bytearray()
|
|
72
|
+
deadline = time.time() + 15.0
|
|
73
|
+
while time.time() < deadline:
|
|
74
|
+
readable, _, _ = select.select([fd], [], [], 0.1)
|
|
75
|
+
if fd in readable:
|
|
76
|
+
try:
|
|
77
|
+
chunk = os.read(fd, 8192)
|
|
78
|
+
except OSError:
|
|
79
|
+
break
|
|
80
|
+
if not chunk:
|
|
81
|
+
break
|
|
82
|
+
out.extend(chunk)
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
waited = os.waitpid(pid, os.WNOHANG)
|
|
86
|
+
if waited[0] == pid:
|
|
87
|
+
while True:
|
|
88
|
+
readable, _, _ = select.select([fd], [], [], 0.05)
|
|
89
|
+
if fd not in readable:
|
|
90
|
+
break
|
|
91
|
+
try:
|
|
92
|
+
chunk = os.read(fd, 8192)
|
|
93
|
+
except OSError:
|
|
94
|
+
chunk = b""
|
|
95
|
+
if not chunk:
|
|
96
|
+
break
|
|
97
|
+
out.extend(chunk)
|
|
98
|
+
break
|
|
99
|
+
|
|
100
|
+
elapsed_ms = int((time.perf_counter() - t0) * 1000)
|
|
101
|
+
print(f"STATUSLINE_ELAPSED_MS={elapsed_ms}", file=sys.stderr)
|
|
102
|
+
sys.stdout.buffer.write(bytes(out))
|
|
103
|
+
return 0
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
if __name__ == "__main__":
|
|
107
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Render statusline VHS goldens (PNG frames) for the committed fixtures.
|
|
3
|
+
#
|
|
4
|
+
# Goldens are platform-sensitive (font rasterization). This script ALWAYS
|
|
5
|
+
# renders inside a pinned linux/amd64 container unless you pass --native
|
|
6
|
+
# (which the container entrypoint uses). That way a macOS developer and
|
|
7
|
+
# GitHub Actions ubuntu-latest produce comparable pixels.
|
|
8
|
+
#
|
|
9
|
+
# Usage (from repo root):
|
|
10
|
+
# bash scripts/ci/render-vhs-goldens.sh # -> scripts/ci/vhs-out/
|
|
11
|
+
# bash scripts/ci/render-vhs-goldens.sh --update # also refresh scripts/ci/goldens/
|
|
12
|
+
#
|
|
13
|
+
# Re-baseline (MUST go through this script / the container — never host macOS vhs):
|
|
14
|
+
# bun run ci:vhs:update
|
|
15
|
+
# # inspect scripts/ci/goldens/*.png, then:
|
|
16
|
+
# git add scripts/ci/goldens/*.png
|
|
17
|
+
#
|
|
18
|
+
# Requires: docker (host path). Inside the container: vhs, ffmpeg, ttyd, bun, jq.
|
|
19
|
+
set -euo pipefail
|
|
20
|
+
|
|
21
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
22
|
+
cd "$ROOT"
|
|
23
|
+
|
|
24
|
+
UPDATE=0
|
|
25
|
+
NATIVE=0
|
|
26
|
+
OUT_DIR="scripts/ci/vhs-out"
|
|
27
|
+
GOLDEN_DIR="scripts/ci/goldens"
|
|
28
|
+
IMAGE_NAME="${VHS_GOLDEN_IMAGE:-coding-buddy-vhs:local}"
|
|
29
|
+
DOCKERFILE="scripts/ci/Dockerfile.vhs"
|
|
30
|
+
|
|
31
|
+
for arg in "$@"; do
|
|
32
|
+
case "$arg" in
|
|
33
|
+
--update) UPDATE=1 ;;
|
|
34
|
+
--native) NATIVE=1 ;;
|
|
35
|
+
-h|--help)
|
|
36
|
+
sed -n '2,20p' "$0"
|
|
37
|
+
exit 0
|
|
38
|
+
;;
|
|
39
|
+
*)
|
|
40
|
+
echo "unknown arg: $arg" >&2
|
|
41
|
+
exit 2
|
|
42
|
+
;;
|
|
43
|
+
esac
|
|
44
|
+
done
|
|
45
|
+
|
|
46
|
+
# ── Host path: build pinned linux/amd64 image and re-exec inside it ──────────
|
|
47
|
+
if [ "$NATIVE" -eq 0 ]; then
|
|
48
|
+
command -v docker >/dev/null 2>&1 || {
|
|
49
|
+
echo "FAIL: docker is required to render platform-matched VHS goldens" >&2
|
|
50
|
+
echo " Install Docker, or if you are already inside scripts/ci/Dockerfile.vhs," >&2
|
|
51
|
+
echo " re-run with --native." >&2
|
|
52
|
+
exit 1
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
echo "build: $IMAGE_NAME (linux/amd64) from $DOCKERFILE"
|
|
56
|
+
docker build \
|
|
57
|
+
--platform linux/amd64 \
|
|
58
|
+
-f "$DOCKERFILE" \
|
|
59
|
+
-t "$IMAGE_NAME" \
|
|
60
|
+
scripts/ci
|
|
61
|
+
|
|
62
|
+
args=("--native")
|
|
63
|
+
[ "$UPDATE" -eq 1 ] && args+=("--update")
|
|
64
|
+
|
|
65
|
+
# Host creates output dirs world-writable so the container's uid 1000 can
|
|
66
|
+
# write through the bind mount (GHA checkout owner is not 1000).
|
|
67
|
+
mkdir -p "$OUT_DIR" "$GOLDEN_DIR"
|
|
68
|
+
chmod a+rwx "$OUT_DIR" "$GOLDEN_DIR" 2>/dev/null || true
|
|
69
|
+
|
|
70
|
+
echo "run: docker render (${args[*]})"
|
|
71
|
+
set +e
|
|
72
|
+
# Match the image USER (vhs:1000). Chromium will not start as root.
|
|
73
|
+
# HOME is the image's /home/vhs so rod can cache its browser there.
|
|
74
|
+
# seccomp=unconfined is required for Chromium under Docker Desktop/OrbStack.
|
|
75
|
+
docker run --rm \
|
|
76
|
+
--platform linux/amd64 \
|
|
77
|
+
--security-opt seccomp=unconfined \
|
|
78
|
+
--user 1000:1000 \
|
|
79
|
+
-e CI= \
|
|
80
|
+
-e COLORTERM=truecolor \
|
|
81
|
+
-e HOME=/home/vhs \
|
|
82
|
+
-e XDG_CACHE_HOME=/home/vhs/.cache \
|
|
83
|
+
-e VHS_GOLDEN_IMAGE="$IMAGE_NAME" \
|
|
84
|
+
-v "$ROOT":/work \
|
|
85
|
+
-w /work \
|
|
86
|
+
"$IMAGE_NAME" \
|
|
87
|
+
bash scripts/ci/render-vhs-goldens.sh "${args[@]}"
|
|
88
|
+
status=$?
|
|
89
|
+
set -e
|
|
90
|
+
|
|
91
|
+
# Fix ownership when the host user is non-root (best-effort).
|
|
92
|
+
if [ "$(id -u)" -ne 0 ]; then
|
|
93
|
+
docker run --rm --platform linux/amd64 \
|
|
94
|
+
--user 0:0 \
|
|
95
|
+
-v "$ROOT":/work -w /work \
|
|
96
|
+
"$IMAGE_NAME" \
|
|
97
|
+
bash -lc "chown -R $(id -u):$(id -g) scripts/ci/vhs-out scripts/ci/goldens 2>/dev/null || true" \
|
|
98
|
+
>/dev/null 2>&1 || true
|
|
99
|
+
fi
|
|
100
|
+
exit "$status"
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
# ── Native path (inside the pinned container, or VHS_NATIVE override) ────────
|
|
104
|
+
command -v vhs >/dev/null 2>&1 || { echo "vhs not on PATH (native mode)" >&2; exit 1; }
|
|
105
|
+
command -v ffmpeg >/dev/null 2>&1 || { echo "ffmpeg not on PATH (native mode)" >&2; exit 1; }
|
|
106
|
+
command -v ttyd >/dev/null 2>&1 || { echo "ttyd not on PATH (native mode)" >&2; exit 1; }
|
|
107
|
+
command -v bun >/dev/null 2>&1 || { echo "bun not on PATH (native mode)" >&2; exit 1; }
|
|
108
|
+
|
|
109
|
+
echo "native render host=$(uname -s)/$(uname -m) vhs=$(vhs --version 2>/dev/null | head -n1)"
|
|
110
|
+
echo "font: $(fc-match 'DejaVu Sans Mono' 2>/dev/null || echo 'fc-match unavailable')"
|
|
111
|
+
|
|
112
|
+
# Prefer cleaning contents over rm -rf of the directory itself: on CI the
|
|
113
|
+
# host owns scripts/ci/vhs-out (created world-writable above) and uid 1000
|
|
114
|
+
# cannot unlink the directory from scripts/.
|
|
115
|
+
rm -rf "${OUT_DIR:?}/"* 2>/dev/null || true
|
|
116
|
+
mkdir -p "$OUT_DIR"
|
|
117
|
+
|
|
118
|
+
env_ci_was="${CI-}"
|
|
119
|
+
unset CI || true
|
|
120
|
+
export COLORTERM=truecolor
|
|
121
|
+
# Pin rows so VHS golden renders stay at full density even when the terminal
|
|
122
|
+
# emulator reports a smaller size. vhs-run-statusline.sh falls back to 50 too.
|
|
123
|
+
export BUDDY_STATUSLINE_ROWS=50
|
|
124
|
+
|
|
125
|
+
# Fixture generation uses only in-repo modules (server/art.ts). Never touch
|
|
126
|
+
# the developer's live ~/.config/claude/buddy-state — CLAUDE_CONFIG_DIR is
|
|
127
|
+
# pointed at temp fixture dirs below.
|
|
128
|
+
gen_dir="$OUT_DIR/fixtures"
|
|
129
|
+
mkdir -p "$gen_dir/cjk/buddy-state" "$gen_dir/emoji/buddy-state"
|
|
130
|
+
cp scripts/ci/statusline-fixture/buddy-state/config.json "$gen_dir/cjk/buddy-state/config.json"
|
|
131
|
+
cp scripts/ci/statusline-fixture/buddy-state/config.json "$gen_dir/emoji/buddy-state/config.json"
|
|
132
|
+
|
|
133
|
+
bun "$ROOT/scripts/ci/write-density-status.ts" "$gen_dir/cjk/buddy-state/status.json" <<'JSON'
|
|
134
|
+
{"name":"Daffodil","species":"duck","rarity":"common","stars":"★","face":"(°>","eye":"°","shiny":false,"hat":"none","reaction":"","muted":false,"achievement":"","level":1,"xp":0,"mood":"focused"}
|
|
135
|
+
JSON
|
|
136
|
+
cp "$gen_dir/cjk/buddy-state/status.json" "$gen_dir/emoji/buddy-state/status.json"
|
|
137
|
+
|
|
138
|
+
bun -e "
|
|
139
|
+
import { writeFileSync } from 'fs';
|
|
140
|
+
writeFileSync(process.argv[1], JSON.stringify({
|
|
141
|
+
reaction: '编译通过了 你好世界', timestamp: Date.now(), reason: 'turn',
|
|
142
|
+
}));
|
|
143
|
+
writeFileSync(process.argv[2], JSON.stringify({
|
|
144
|
+
reaction: 'ship it 🎉 ❤️ ✨', timestamp: Date.now(), reason: 'success',
|
|
145
|
+
}));
|
|
146
|
+
" \
|
|
147
|
+
"$gen_dir/cjk/buddy-state/reaction.default.json" \
|
|
148
|
+
"$gen_dir/emoji/buddy-state/reaction.default.json"
|
|
149
|
+
|
|
150
|
+
render_one() {
|
|
151
|
+
local name="$1"
|
|
152
|
+
local cols="$2"
|
|
153
|
+
local lines="$3"
|
|
154
|
+
local fixture_rel="$4"
|
|
155
|
+
local tape out_png out_gif
|
|
156
|
+
tape="$OUT_DIR/${name}.tape"
|
|
157
|
+
out_png="$OUT_DIR/${name}.png"
|
|
158
|
+
out_gif="$OUT_DIR/${name}.gif"
|
|
159
|
+
|
|
160
|
+
# Relative paths only — vhs's tape parser treats absolute "/..." paths as
|
|
161
|
+
# separate commands. JSON stays out of the tape via vhs-run-statusline.sh.
|
|
162
|
+
#
|
|
163
|
+
# Hide/Show: the Type+Enter setup must not appear in the golden. Previous
|
|
164
|
+
# macOS baselines accidentally baked the shell command into the frame, which
|
|
165
|
+
# made pixel goldens useless as a layout check.
|
|
166
|
+
cat > "$tape" <<EOF
|
|
167
|
+
Output ${out_gif}
|
|
168
|
+
Set Width $(( cols * 9 ))
|
|
169
|
+
Set Height $(( lines * 18 ))
|
|
170
|
+
Set FontSize 14
|
|
171
|
+
Set FontFamily "DejaVu Sans Mono"
|
|
172
|
+
Set Padding 12
|
|
173
|
+
Set Margin 0
|
|
174
|
+
Set Theme "Catppuccin Mocha"
|
|
175
|
+
Set Shell "bash"
|
|
176
|
+
Set TypingSpeed 0ms
|
|
177
|
+
Set Framerate 10
|
|
178
|
+
Hide
|
|
179
|
+
Type "PS1=; tput civis 2>/dev/null; clear; bash scripts/ci/vhs-run-statusline.sh ${cols} ${fixture_rel}"
|
|
180
|
+
Enter
|
|
181
|
+
Sleep 800ms
|
|
182
|
+
Show
|
|
183
|
+
Sleep 2s
|
|
184
|
+
EOF
|
|
185
|
+
|
|
186
|
+
echo "render: $name (cols=$cols fixture=$fixture_rel)"
|
|
187
|
+
vhs "$tape"
|
|
188
|
+
[ -f "$out_gif" ] || { echo "missing gif: $out_gif" >&2; exit 1; }
|
|
189
|
+
|
|
190
|
+
# Extract the final settled frame as a still PNG.
|
|
191
|
+
local nframes last
|
|
192
|
+
nframes=$(ffprobe -v error -select_streams v:0 -count_frames \
|
|
193
|
+
-show_entries stream=nb_read_frames -of csv=p=0 "$out_gif" || echo 1)
|
|
194
|
+
last=0
|
|
195
|
+
if [ "${nframes:-0}" -gt 1 ] 2>/dev/null; then
|
|
196
|
+
last=$(( nframes - 1 ))
|
|
197
|
+
fi
|
|
198
|
+
ffmpeg -hide_banner -loglevel error -y \
|
|
199
|
+
-i "$out_gif" \
|
|
200
|
+
-vf "select=eq(n\,${last})" \
|
|
201
|
+
-update 1 -frames:v 1 \
|
|
202
|
+
"$out_png"
|
|
203
|
+
[ -f "$out_png" ] || { echo "missing png: $out_png" >&2; exit 1; }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
render_one "baseline-80" 80 18 "scripts/ci/statusline-fixture"
|
|
207
|
+
render_one "baseline-40" 40 18 "scripts/ci/statusline-fixture"
|
|
208
|
+
render_one "cjk-80" 80 18 "scripts/ci/vhs-out/fixtures/cjk"
|
|
209
|
+
# Emoji reaction needs room for the bubble (INNER_W default 44). Narrow
|
|
210
|
+
# coverage lives in baseline-40; this fixture is about glyph rasterization.
|
|
211
|
+
render_one "emoji-80" 80 18 "scripts/ci/vhs-out/fixtures/emoji"
|
|
212
|
+
|
|
213
|
+
if [ "$UPDATE" -eq 1 ]; then
|
|
214
|
+
mkdir -p "$GOLDEN_DIR"
|
|
215
|
+
for png in "$OUT_DIR"/*.png; do
|
|
216
|
+
base=$(basename "$png")
|
|
217
|
+
cp "$png" "$GOLDEN_DIR/$base"
|
|
218
|
+
echo "updated golden: $GOLDEN_DIR/$base"
|
|
219
|
+
done
|
|
220
|
+
fi
|
|
221
|
+
|
|
222
|
+
if [ -n "${env_ci_was}" ]; then
|
|
223
|
+
export CI="$env_ci_was"
|
|
224
|
+
fi
|
|
225
|
+
|
|
226
|
+
echo "OK: vhs renders written to $OUT_DIR (native=$(uname -s)/$(uname -m))"
|
|
@@ -3,7 +3,33 @@ Set Width 1400
|
|
|
3
3
|
Set Height 500
|
|
4
4
|
Set FontSize 14
|
|
5
5
|
Set Shell "bash"
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
# Generate temporary full/compact/minimal fixtures from the committed baseline
|
|
8
|
+
# without touching production fixtures. The helper augments status.json with
|
|
9
|
+
# compactFrames/minimalFrames when getStatusFrames() emits them.
|
|
10
|
+
Hide
|
|
11
|
+
Type "bash scripts/ci/vhs-density-fixtures.sh"
|
|
7
12
|
Enter
|
|
8
|
-
Sleep
|
|
13
|
+
Sleep 2s
|
|
14
|
+
Show
|
|
15
|
+
|
|
16
|
+
Type "clear"
|
|
17
|
+
Enter
|
|
18
|
+
Sleep 500ms
|
|
19
|
+
|
|
20
|
+
# Full density: 50 rows gives the six-ish-line art + optional bubble.
|
|
21
|
+
Type "COLUMNS=80 BUDDY_STATUSLINE_ROWS=50 bash scripts/ci/vhs-run-statusline.sh 80 scripts/ci/vhs-out/density-fixtures/full"
|
|
22
|
+
Enter
|
|
23
|
+
Sleep 2s
|
|
24
|
+
|
|
25
|
+
# Compact density: three art rows + name, no bubble.
|
|
26
|
+
Type "COLUMNS=80 BUDDY_STATUSLINE_ROWS=50 bash scripts/ci/vhs-run-statusline.sh 80 scripts/ci/vhs-out/density-fixtures/compact"
|
|
27
|
+
Enter
|
|
28
|
+
Sleep 2s
|
|
29
|
+
|
|
30
|
+
# Minimal density: a single sprite/face + name line.
|
|
31
|
+
Type "COLUMNS=80 BUDDY_STATUSLINE_ROWS=50 bash scripts/ci/vhs-run-statusline.sh 80 scripts/ci/vhs-out/density-fixtures/minimal"
|
|
32
|
+
Enter
|
|
33
|
+
Sleep 2s
|
|
34
|
+
|
|
9
35
|
Screenshot scripts/ci/statusline.png
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Generate temporary full/compact/minimal statusline fixtures for the VHS tape.
|
|
3
|
+
# Leaves the committed production fixture untouched and writes into the ignored
|
|
4
|
+
# scripts/ci/vhs-out/ workspace.
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
8
|
+
cd "$ROOT"
|
|
9
|
+
|
|
10
|
+
src_status="$ROOT/scripts/ci/statusline-fixture/buddy-state/status.json"
|
|
11
|
+
out_dir="$ROOT/scripts/ci/vhs-out/density-fixtures"
|
|
12
|
+
|
|
13
|
+
rm -rf "$out_dir"
|
|
14
|
+
mkdir -p "$out_dir"/{full,compact,minimal}/buddy-state
|
|
15
|
+
|
|
16
|
+
for d in full compact minimal; do
|
|
17
|
+
cp "$src_status" "$out_dir/$d/buddy-state/status.json"
|
|
18
|
+
printf '{"useCombinedStatus":false,"statuslineDensity":"%s"}\n' "$d" \
|
|
19
|
+
> "$out_dir/$d/buddy-state/config.json"
|
|
20
|
+
done
|
|
21
|
+
|
|
22
|
+
# Augment the committed fixture with full/compact/minimal frame arrays.
|
|
23
|
+
cat "$src_status" | bun "$ROOT/scripts/ci/write-density-status.ts" "$out_dir/full/buddy-state/status.json"
|
|
24
|
+
cp "$out_dir/full/buddy-state/status.json" "$out_dir/compact/buddy-state/status.json"
|
|
25
|
+
cp "$out_dir/full/buddy-state/status.json" "$out_dir/minimal/buddy-state/status.json"
|
|
26
|
+
|
|
27
|
+
echo "$out_dir"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Invoked from VHS tapes. Args: <cols> <fixture-relpath-from-repo-root>
|
|
3
|
+
# BUDDY_STATUSLINE_ROWS defaults to 50 (full density) so golden renders are not
|
|
4
|
+
# sensitive to the VHS terminal's actual row count.
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
7
|
+
cols="${1:?cols}"
|
|
8
|
+
fixture_rel="${2:?fixture}"
|
|
9
|
+
payload='{"session_id":"ci","model":{"display_name":"CI"},"workspace":{"current_dir":"."}}'
|
|
10
|
+
|
|
11
|
+
if [[ "$fixture_rel" = /* ]]; then
|
|
12
|
+
cfg_dir="$fixture_rel"
|
|
13
|
+
else
|
|
14
|
+
cfg_dir="$ROOT/$fixture_rel"
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
rows="${BUDDY_STATUSLINE_ROWS:-50}"
|
|
18
|
+
|
|
19
|
+
cd "$ROOT"
|
|
20
|
+
printf '%s' "$payload" \
|
|
21
|
+
| COLUMNS="$cols" \
|
|
22
|
+
BUDDY_FAKE_NOW=0 \
|
|
23
|
+
BUDDY_STATUSLINE_ROWS="$rows" \
|
|
24
|
+
CLAUDE_CONFIG_DIR="$cfg_dir" \
|
|
25
|
+
CLAUDE_CODE_SESSION_ID= \
|
|
26
|
+
TMUX_PANE= \
|
|
27
|
+
BUDDY_SHELL= \
|
|
28
|
+
bash statusline/buddy-status.sh
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Derives a status.json fixture with full/compact/minimal frame arrays.
|
|
4
|
+
*
|
|
5
|
+
* Reads a base status object from stdin, computes frames from species/eye/hat,
|
|
6
|
+
* and writes the augmented object to the file named by the first argument.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* cat base-status.json | bun scripts/ci/write-density-status.ts dest.json
|
|
10
|
+
* bun scripts/ci/write-density-status.ts dest.json < base-status.json
|
|
11
|
+
*/
|
|
12
|
+
import { writeFileSync } from "fs";
|
|
13
|
+
import { getStatusFrames } from "../../server/art.ts";
|
|
14
|
+
import { renderFace, resolveEyeGlyph, type Species } from "../../core/engine.ts";
|
|
15
|
+
|
|
16
|
+
function deriveCompactFrames(fullFrames: string[]): string[] {
|
|
17
|
+
return fullFrames.map((body) => {
|
|
18
|
+
const lines = body.split("\n");
|
|
19
|
+
const selected: string[] = [];
|
|
20
|
+
for (const line of lines) {
|
|
21
|
+
if (selected.length >= 3) break;
|
|
22
|
+
if (line.trim() !== "") selected.push(line);
|
|
23
|
+
}
|
|
24
|
+
while (selected.length < 3) selected.push("");
|
|
25
|
+
return selected.join("\n");
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function deriveMinimalFrames(species: Species, eye: string): string[] {
|
|
30
|
+
const face = renderFace(species, resolveEyeGlyph(eye));
|
|
31
|
+
return Array.from({ length: 4 }, () => face);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function main() {
|
|
35
|
+
const dest = process.argv[2];
|
|
36
|
+
if (!dest) {
|
|
37
|
+
console.error("usage: write-density-status.ts <dest.json>");
|
|
38
|
+
process.exit(2);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let input = "";
|
|
42
|
+
for await (const chunk of process.stdin) {
|
|
43
|
+
input += chunk;
|
|
44
|
+
}
|
|
45
|
+
const base = input ? JSON.parse(input) : {};
|
|
46
|
+
|
|
47
|
+
const bones = {
|
|
48
|
+
species: base.species ?? "pikachu",
|
|
49
|
+
rarity: base.rarity ?? "common",
|
|
50
|
+
eye: base.eye ?? "o",
|
|
51
|
+
hat: base.hat ?? "none",
|
|
52
|
+
shiny: base.shiny ?? false,
|
|
53
|
+
stats: base.stats ?? { DEBUGGING: 50, PATIENCE: 50, CHAOS: 50, WISDOM: 50, SNARK: 50 },
|
|
54
|
+
peak: base.peak ?? "DEBUGGING",
|
|
55
|
+
dump: base.dump ?? "PATIENCE",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const result = getStatusFrames(bones) as {
|
|
59
|
+
frames: string[];
|
|
60
|
+
frameSequence: number[];
|
|
61
|
+
compactFrames?: string[];
|
|
62
|
+
minimalFrames?: string[];
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const compactFrames =
|
|
66
|
+
result.compactFrames && result.compactFrames.length > 0
|
|
67
|
+
? result.compactFrames
|
|
68
|
+
: deriveCompactFrames(result.frames);
|
|
69
|
+
|
|
70
|
+
const minimalFrames =
|
|
71
|
+
result.minimalFrames && result.minimalFrames.length > 0
|
|
72
|
+
? result.minimalFrames
|
|
73
|
+
: deriveMinimalFrames(bones.species as Species, bones.eye);
|
|
74
|
+
|
|
75
|
+
const out = {
|
|
76
|
+
...base,
|
|
77
|
+
frames: result.frames,
|
|
78
|
+
compactFrames,
|
|
79
|
+
minimalFrames,
|
|
80
|
+
frameSequence: result.frameSequence,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
writeFileSync(dest, JSON.stringify(out));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
main();
|
|
@@ -5,30 +5,54 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Regenerate: bun run gen:emoji-widths
|
|
7
7
|
*
|
|
8
|
-
* The emitted list holds
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* ambiguous Miscellaneous Symbols + Dingbats block.
|
|
12
|
-
*/
|
|
8
|
+
* The emitted list holds every codepoint with Emoji_Presentation=Yes, so the
|
|
9
|
+
* bash width walker stays in parity with server/art.ts for both BMP symbols
|
|
10
|
+
* and supplementary-plane emoji.
|
|
13
11
|
|
|
12
|
+
*/
|
|
14
13
|
import { writeFileSync } from "fs";
|
|
15
14
|
import { join } from "path";
|
|
16
15
|
|
|
17
16
|
const OUT = join(import.meta.dir, "..", "statusline", "emoji-widths.data");
|
|
17
|
+
const TEXT_OUT = join(import.meta.dir, "..", "statusline", "emoji-text.data");
|
|
18
18
|
|
|
19
19
|
const EMOJI_PRES_RE = /\p{Emoji_Presentation}/u;
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
const EMOJI_RE = /\p{Emoji}/u;
|
|
21
|
+
const presentation: number[] = [];
|
|
22
|
+
const textDefault: number[] = [];
|
|
23
|
+
for (let cp = 0; cp <= 0x10FFFF; cp++) {
|
|
24
|
+
const character = String.fromCodePoint(cp);
|
|
25
|
+
if (EMOJI_PRES_RE.test(character)) presentation.push(cp);
|
|
26
|
+
else if (EMOJI_RE.test(character)) textDefault.push(cp);
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
codepoints.
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
function encodeRanges(codepoints: number[]): string {
|
|
30
|
+
const ranges: string[] = [];
|
|
31
|
+
let start = codepoints[0];
|
|
32
|
+
let end = start;
|
|
33
|
+
for (const codepoint of codepoints.slice(1)) {
|
|
34
|
+
if (codepoint === end + 1) {
|
|
35
|
+
end = codepoint;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
ranges.push(start === end ? String(start) : `${start}-${end}`);
|
|
39
|
+
start = end = codepoint;
|
|
40
|
+
}
|
|
41
|
+
ranges.push(start === end ? String(start) : `${start}-${end}`);
|
|
42
|
+
return ranges.join(",");
|
|
43
|
+
}
|
|
32
44
|
|
|
33
|
-
|
|
34
|
-
|
|
45
|
+
const header = "# Auto-generated by scripts/gen-emoji-widths.ts — do not edit by hand.";
|
|
46
|
+
writeFileSync(OUT, [
|
|
47
|
+
header,
|
|
48
|
+
"# Decimal codepoint ranges with Unicode Emoji_Presentation=Yes.",
|
|
49
|
+
encodeRanges(presentation),
|
|
50
|
+
"",
|
|
51
|
+
].join("\n"));
|
|
52
|
+
writeFileSync(TEXT_OUT, [
|
|
53
|
+
header,
|
|
54
|
+
"# Decimal codepoint ranges with Unicode Emoji=Yes and Emoji_Presentation=No.",
|
|
55
|
+
encodeRanges(textDefault),
|
|
56
|
+
"",
|
|
57
|
+
].join("\n"));
|
|
58
|
+
console.log(`Wrote ${presentation.length} presentation and ${textDefault.length} text-default codepoints`);
|
package/scripts/paths.sh
CHANGED
|
@@ -31,7 +31,9 @@ else
|
|
|
31
31
|
CLAUDE_USER_CONFIG="$HOME/.claude.json"
|
|
32
32
|
fi
|
|
33
33
|
|
|
34
|
-
if [[ -n "${
|
|
34
|
+
if [[ -n "${CODING_BUDDY_USER_ID:-}" ]]; then
|
|
35
|
+
BUDDY_STATE_DIR="${CODING_BUDDY_STATE_DIR:-$HOME/.coding-buddy/shared}"
|
|
36
|
+
elif [[ -n "${CLAUDE_CONFIG_DIR:-}" ]]; then
|
|
35
37
|
BUDDY_STATE_DIR="$CLAUDE_CONFIG_DIR/buddy-state"
|
|
36
38
|
else
|
|
37
39
|
BUDDY_STATE_DIR="$HOME/.claude-buddy"
|