@maxiaochao/pi-toolkit 0.2.5 → 0.3.0
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/CHANGELOG.md +8 -0
- package/README.md +16 -1
- package/bin/pi-worktree +200 -0
- package/bin/pi-worktree-completion.bash +36 -0
- package/extensions/cache-export/render.ts +32 -4
- package/extensions/cache-export/tests/e2e-browser.mjs +21 -1
- package/extensions/cache-export/tests/run-tests.mjs +18 -1
- package/package.json +5 -1
- package/scripts/install-agents.mjs +23 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## 2026-09-11 - v0.3.0
|
|
2
|
+
|
|
3
|
+
- add pi-worktree CLI: one-command worktree + Pi launch with list/info/out/remove/prune and bash completion (postinstall to ~/.local/bin)
|
|
4
|
+
|
|
5
|
+
## 2026-09-09 - v0.2.6
|
|
6
|
+
|
|
7
|
+
- fix cache export: segment-view tooltip and event markers now show global request numbers (aligned with the x-axis) instead of relative 1-based; e2e asserts real tooltip values
|
|
8
|
+
|
|
1
9
|
## 2026-09-09 - v0.2.5
|
|
2
10
|
|
|
3
11
|
- fix cache export: segment views lost the hover tooltip (it rendered inside the hidden all-history section); move to one page-level tooltip; e2e now asserts real rendering
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pi-toolkit
|
|
2
2
|
|
|
3
|
-
Personal Pi toolkit with the `/todos`, `/btw`, and `/cache_export` extensions, bundled agent skills including Chrome/Chromium automation,
|
|
3
|
+
Personal Pi toolkit with the `/todos`, `/btw`, and `/cache_export` extensions, bundled agent skills including Chrome/Chromium automation, the Night Owl theme, and the `pi-worktree` CLI.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -44,6 +44,21 @@ Use `/cache_export [path]` to write an interactive per-request cache dashboard.
|
|
|
44
44
|
|
|
45
45
|
The bundled `nightowl` theme is available to Pi through the package manifest and is selected automatically on npm installation only when no theme is already configured.
|
|
46
46
|
|
|
47
|
+
## pi-worktree CLI
|
|
48
|
+
|
|
49
|
+
The package also ships a standalone shell command, `pi-worktree`, that wraps Git worktree creation and Pi launch into one step. On installation (Unix-like systems) the postinstall hook copies it to `~/.local/bin/pi-worktree` and installs bash completion to `~/.local/share/bash-completion/completions/pi-worktree` (no `.bashrc` edits needed).
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pi-worktree feat-ui # create/reuse .worktrees/feat-ui, branch feat-ui, launch pi inside
|
|
53
|
+
pi-worktree list # list worktrees (main checkout marked "main checkout")
|
|
54
|
+
pi-worktree info feat-ui # base commit, ahead/behind main, unique commits
|
|
55
|
+
pi-worktree out # open a shell in the main checkout (for merging)
|
|
56
|
+
pi-worktree remove feat-ui # remove the worktree (refuses unmerged branches)
|
|
57
|
+
pi-worktree prune # drop stale registrations for manually deleted dirs
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Typical loop: `pi-worktree <name>` to start, commit inside Pi, `pi-worktree out` + `git merge <name>` to land the work, `pi-worktree remove <name>` to clean up. New branches are created from `origin/main` (falling back to `main`, then `HEAD`), and the base commit is recorded in `branch.<name>.base` config so `info` can always tell you where the branch came from. Bash completion covers subcommands and existing worktree names (`pi-worktree feat<Tab>`).
|
|
61
|
+
|
|
47
62
|
The bundled `web-browser` skill provides reusable scripts for starting Chrome/Chromium with remote debugging, navigating tabs, evaluating JavaScript, emulating devices, taking screenshots, dismissing cookie dialogs, and inspecting browser logs. It auto-detects common Chrome/Chromium installations on macOS and Linux, including Windows Chrome when Pi runs under WSL; set `BROWSER_BIN` when the binary is elsewhere.
|
|
48
63
|
|
|
49
64
|
The browser skill can be loaded explicitly with `/skill:web-browser`. Its scripts are available relative to the skill directory, for example:
|
package/bin/pi-worktree
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# pi-worktree: create/reuse a git worktree, then launch pi inside it.
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# pi-worktree [name] [pi args...] # enter worktree <name> and start pi
|
|
6
|
+
# pi-worktree -l | --list # list worktrees (main checkout marked "main checkout")
|
|
7
|
+
# pi-worktree -i <name> # show base commit, ahead/behind, unique commits
|
|
8
|
+
# pi-worktree -o | --out # cd back to the main checkout
|
|
9
|
+
# pi-worktree -r <name> # remove worktree <name> (must be clean & merged)
|
|
10
|
+
# pi-worktree -p | --prune # drop stale worktree registrations
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
# Colors: only when stdout is a TTY and NO_COLOR is unset (no-color.org)
|
|
14
|
+
if [[ -t 1 && -z "${NO_COLOR:-}" ]]; then
|
|
15
|
+
C_BOLD=$'\e[1m' C_DIM=$'\e[2m' C_RED=$'\e[31m' C_GREEN=$'\e[32m' C_YELLOW=$'\e[33m' C_CYAN=$'\e[36m' C_RESET=$'\e[0m'
|
|
16
|
+
else
|
|
17
|
+
C_BOLD='' C_DIM='' C_RED='' C_GREEN='' C_YELLOW='' C_CYAN='' C_RESET=''
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
ok() { printf '%spi-worktree:%s %s\n' "$C_GREEN" "$C_RESET" "$*"; }
|
|
21
|
+
warn() { printf '%s⚠️ %s%s\n' "$C_YELLOW" "$C_RESET" "$*"; }
|
|
22
|
+
die() { printf '%spi-worktree: %s%s\n' "$C_RED" "$C_RESET" "$*" >&2; exit 1; }
|
|
23
|
+
|
|
24
|
+
require_repo() {
|
|
25
|
+
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || die "not inside a git repository"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
main_checkout() {
|
|
29
|
+
git rev-parse --path-format=absolute --git-common-dir | sed 's|/.git$||'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
in_linked_worktree() {
|
|
33
|
+
local git_dir common_dir
|
|
34
|
+
git_dir=$(git rev-parse --git-dir 2>/dev/null) || return 1
|
|
35
|
+
common_dir=$(git rev-parse --git-common-dir 2>/dev/null) || return 1
|
|
36
|
+
[[ "$(cd "$git_dir" && pwd -P)" != "$(cd "$common_dir" && pwd -P)" ]] \
|
|
37
|
+
&& [[ -z "$(git rev-parse --show-superproject-working-tree 2>/dev/null)" ]]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
wt_dir() { echo ".worktrees/$1"; }
|
|
41
|
+
|
|
42
|
+
# Best available base ref: origin/main > main > HEAD
|
|
43
|
+
pick_base() {
|
|
44
|
+
if git show-ref --verify --quiet refs/remotes/origin/main; then
|
|
45
|
+
echo origin/main
|
|
46
|
+
elif git show-ref --verify --quiet refs/heads/main; then
|
|
47
|
+
echo main
|
|
48
|
+
else
|
|
49
|
+
echo HEAD
|
|
50
|
+
fi
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
ensure_gitignore() {
|
|
54
|
+
if ! git check-ignore -q .worktrees 2>/dev/null; then
|
|
55
|
+
printf '.worktrees/\n' >> .gitignore
|
|
56
|
+
ok "added .worktrees/ to .gitignore"
|
|
57
|
+
fi
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
cmd_list() {
|
|
61
|
+
require_repo
|
|
62
|
+
local main_dir; main_dir=$(main_checkout)
|
|
63
|
+
# Annotate the primary checkout and stale (prunable) entries.
|
|
64
|
+
git worktree list --porcelain | awk -v main="$main_dir" \
|
|
65
|
+
-v D="${C_DIM}" -v Y="${C_YELLOW}" -v C="${C_CYAN}" -v R="${C_RESET}" '
|
|
66
|
+
/^worktree / { path=substr($0,10); next }
|
|
67
|
+
/^HEAD / { sha=substr($0,6); next }
|
|
68
|
+
/^branch / { b=substr($0,8); sub("refs/heads/","",b); next }
|
|
69
|
+
/^detached/ { b="(detached)"; next }
|
|
70
|
+
/^bare/ { b="(bare)"; next }
|
|
71
|
+
/^prunable/ { stale=1; next }
|
|
72
|
+
/^$/ {
|
|
73
|
+
if (b=="") b="?"
|
|
74
|
+
mark=(path==main) ? " " D "(main checkout)" R : ""
|
|
75
|
+
branch=(path==main) ? b : C b R
|
|
76
|
+
hint=(stale) ? " " Y "← stale: dir gone, run: pi-worktree prune" R : ""
|
|
77
|
+
printf "%s %s%s%s %s%s%s\n", path, D, substr(sha,1,7), R, branch, mark, hint
|
|
78
|
+
path=""; sha=""; b=""; stale=0
|
|
79
|
+
}
|
|
80
|
+
'
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
subcmd_prune() {
|
|
84
|
+
require_repo
|
|
85
|
+
git worktree prune -v
|
|
86
|
+
ok "pruned stale worktree registrations"
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
cmd_out() {
|
|
90
|
+
cd "$(main_checkout)" && exec "${SHELL:-/bin/bash}"
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
cmd_info() {
|
|
94
|
+
require_repo
|
|
95
|
+
[[ $# -eq 1 ]] || die "usage: pi-worktree -i <name>"
|
|
96
|
+
local dir; dir="$(wt_dir "$1")"
|
|
97
|
+
[[ -d "$dir" ]] || die "worktree not found: $dir"
|
|
98
|
+
local branch; branch=$(git -C "$dir" branch --show-current)
|
|
99
|
+
echo "worktree: $dir"
|
|
100
|
+
if git -C "$dir" diff --quiet 2>/dev/null && git -C "$dir" diff --cached --quiet 2>/dev/null \
|
|
101
|
+
&& [[ -z "$(git -C "$dir" ls-files --others --exclude-standard)" ]]; then
|
|
102
|
+
echo "status: clean"
|
|
103
|
+
else
|
|
104
|
+
echo "status: DIRTY"
|
|
105
|
+
git -C "$dir" status --short | head -10
|
|
106
|
+
fi
|
|
107
|
+
echo "branch: $branch @ $(git -C "$dir" rev-parse --short HEAD)"
|
|
108
|
+
local base baseRef ahead behind
|
|
109
|
+
base=$(git config --get "branch.$branch.base" 2>/dev/null || true)
|
|
110
|
+
baseRef=$(git config --get "branch.$branch.baseRef" 2>/dev/null || true)
|
|
111
|
+
if [[ -n "$base" ]]; then
|
|
112
|
+
echo "based on: $baseRef @ $base"
|
|
113
|
+
ahead=$(git rev-list --count "$base..$branch" 2>/dev/null || echo "?")
|
|
114
|
+
echo "ahead: $ahead commits since base"
|
|
115
|
+
else
|
|
116
|
+
echo "based on: (not recorded; run: git merge-base main $branch)"
|
|
117
|
+
fi
|
|
118
|
+
if git show-ref --verify --quiet refs/heads/main && [[ "$branch" != main ]]; then
|
|
119
|
+
behind=$(git rev-list --count "$branch..main" 2>/dev/null || echo "?")
|
|
120
|
+
echo "behind: $behind commits vs main"$([[ "${behind:-0}" -gt 0 ]] && echo " ← consider rebase")
|
|
121
|
+
fi
|
|
122
|
+
if [[ -n "$base" ]]; then
|
|
123
|
+
echo "unique commits:"
|
|
124
|
+
git log --oneline "$base..$branch" | head -5 || true
|
|
125
|
+
fi
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
cmd_remove() {
|
|
129
|
+
require_repo
|
|
130
|
+
[[ $# -eq 1 ]] || die "usage: pi-worktree -r <name>"
|
|
131
|
+
local dir; dir="$(wt_dir "$1")"
|
|
132
|
+
[[ -d "$dir" ]] || die "worktree not found: $dir"
|
|
133
|
+
# refuse to remove if the branch is not merged into main
|
|
134
|
+
local branch; branch=$(git -C "$dir" branch --show-current)
|
|
135
|
+
if [[ -n "$branch" ]] && ! git merge-base --is-ancestor "$branch" main 2>/dev/null; then
|
|
136
|
+
warn "branch '$branch' does not appear merged into 'main' (or no main branch exists)."
|
|
137
|
+
read -rp "Remove anyway? [y/N] " ans
|
|
138
|
+
[[ "$ans" =~ ^[Yy]$ ]] || die "aborted"
|
|
139
|
+
fi
|
|
140
|
+
git worktree remove "$dir" && ok "removed: $dir"
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
cd_wt() {
|
|
144
|
+
local name="$1"; shift
|
|
145
|
+
require_repo
|
|
146
|
+
|
|
147
|
+
# Already isolated? Just launch pi here (superpowers Step 0).
|
|
148
|
+
if in_linked_worktree; then
|
|
149
|
+
ok "already in an isolated worktree ($(pwd)), launching pi here"
|
|
150
|
+
exec pi "$@"
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
# Dirty workspace: uncommitted changes do NOT follow into the worktree.
|
|
154
|
+
if ! git diff --quiet || ! git diff --cached --quiet || [[ -n "$(git ls-files --others --exclude-standard)" ]]; then
|
|
155
|
+
warn "Uncommitted changes stay in THIS directory; they will NOT appear in the worktree:"
|
|
156
|
+
git status --short | head -20
|
|
157
|
+
read -rp "Continue? [y/N] " ans
|
|
158
|
+
[[ "$ans" =~ ^[Yy]$ ]] || die "aborted"
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
local dir; dir="$(wt_dir "$name")"
|
|
162
|
+
ensure_gitignore
|
|
163
|
+
|
|
164
|
+
if [[ -d "$dir" ]]; then
|
|
165
|
+
# Reuse: must be a registered worktree for this repo
|
|
166
|
+
git -C "$dir" rev-parse --git-dir >/dev/null 2>&1 || die "$dir exists but is not a git worktree"
|
|
167
|
+
ok "reusing existing worktree $dir"
|
|
168
|
+
else
|
|
169
|
+
mkdir -p .worktrees
|
|
170
|
+
# Reuse branch if it exists, else create it
|
|
171
|
+
if git show-ref --verify --quiet "refs/heads/$name"; then
|
|
172
|
+
git worktree add "$dir" "$name"
|
|
173
|
+
ok "checked out existing branch '$name'"
|
|
174
|
+
# record a best-effort base for -i display
|
|
175
|
+
local mb; mb=$(git merge-base main "$name" 2>/dev/null || true)
|
|
176
|
+
[[ -n "$mb" ]] && git config "branch.$name.base" "$(git rev-parse --short "$mb")" \
|
|
177
|
+
&& git config "branch.$name.baseRef" main
|
|
178
|
+
else
|
|
179
|
+
local base; base=$(pick_base)
|
|
180
|
+
git worktree add "$dir" -b "$name" "$base"
|
|
181
|
+
git config "branch.$name.base" "$(git rev-parse --short "$base")"
|
|
182
|
+
git config "branch.$name.baseRef" "$base"
|
|
183
|
+
ok "created branch '$name' from $base @ $(git rev-parse --short "$base")"
|
|
184
|
+
fi
|
|
185
|
+
fi
|
|
186
|
+
|
|
187
|
+
cd "$dir"
|
|
188
|
+
exec pi "$@"
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
case "${1:-}" in
|
|
192
|
+
-l|--list|list|ls) shift; cmd_list "$@" ;;
|
|
193
|
+
-i|--info|info) shift; cmd_info "$@" ;;
|
|
194
|
+
-o|--out|out) shift; cmd_out "$@" ;;
|
|
195
|
+
-r|--remove|rm|remove) shift; cmd_remove "$@" ;;
|
|
196
|
+
-p|--prune|prune) shift; subcmd_prune ;;
|
|
197
|
+
-h|--help|help) sed -n '2,10p' "$0" | sed 's/^# \{0,1\}//' ;;
|
|
198
|
+
"") die "usage: pi-worktree <name> [pi args...] (or list / info / out / remove / prune)" ;;
|
|
199
|
+
*) name="$1"; shift; cd_wt "$name" "$@" ;;
|
|
200
|
+
esac
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# bash completion for pi-worktree
|
|
2
|
+
_pi_worktree() {
|
|
3
|
+
local cur first
|
|
4
|
+
cur="${COMP_WORDS[COMP_CWORD]}"
|
|
5
|
+
first="${COMP_WORDS[1]}"
|
|
6
|
+
|
|
7
|
+
local SUBS="list ls info out remove rm prune -l --list -i --info -o --out -r --remove -p --prune -h --help"
|
|
8
|
+
|
|
9
|
+
# existing worktree names (only linked worktrees, i.e. under .worktrees/)
|
|
10
|
+
local wts=""
|
|
11
|
+
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
12
|
+
local main_dir
|
|
13
|
+
main_dir=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null | sed 's|/.git$||')
|
|
14
|
+
wts=$(git worktree list --porcelain 2>/dev/null \
|
|
15
|
+
| sed -n 's|^worktree ||p' \
|
|
16
|
+
| while read -r p; do [[ "$p" != "$main_dir" ]] && basename "$p"; done || true)
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
if (( COMP_CWORD == 1 )); then
|
|
20
|
+
# first word: subcommands, flags, and existing worktree names (to enter)
|
|
21
|
+
COMPREPLY=($(compgen -W "$SUBS $wts" -- "$cur"))
|
|
22
|
+
return
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
case "$first" in
|
|
26
|
+
-i|--info|info|-r|--remove|remove|rm)
|
|
27
|
+
# second word of info/remove: worktree names only
|
|
28
|
+
COMPREPLY=($(compgen -W "$wts" -- "$cur"))
|
|
29
|
+
;;
|
|
30
|
+
*)
|
|
31
|
+
# after a name: default (file) completion for pi args
|
|
32
|
+
COMPREPLY=()
|
|
33
|
+
;;
|
|
34
|
+
esac
|
|
35
|
+
}
|
|
36
|
+
complete -F _pi_worktree pi-worktree
|
|
@@ -205,8 +205,12 @@ function lineChart(title, series, opts = {}) {
|
|
|
205
205
|
];
|
|
206
206
|
|
|
207
207
|
const tsAttr = timestamps ? ` data-timestamps="${esc(timestamps.join("|"), true)}"` : "";
|
|
208
|
+
// Global per-request numbers (same length as the series) so a small page
|
|
209
|
+
// script can rewrite tau's relative "Request N" tooltip line to the global
|
|
210
|
+
// number inside segment views (tau hardcodes index + 1).
|
|
211
|
+
const reqAttr = requestNumbers ? ` data-requestnumbers="${esc(requestNumbers.join("|"), true)}"` : "";
|
|
208
212
|
const parts = [
|
|
209
|
-
`<svg class="usage-chart" viewBox="0 0 ${width} ${height}" role="img" aria-label="${esc(title, true)}" data-left="${left}" data-right="${right}" data-count="${count}"${tsAttr}>`,
|
|
213
|
+
`<svg class="usage-chart" viewBox="0 0 ${width} ${height}" role="img" aria-label="${esc(title, true)}" data-left="${left}" data-right="${right}" data-count="${count}"${tsAttr}${reqAttr}>`,
|
|
210
214
|
`<text class="chart-title" x="${left}" y="24">${esc(title)}</text>`,
|
|
211
215
|
];
|
|
212
216
|
for (let tick = 0; tick < 6; tick++) {
|
|
@@ -233,7 +237,10 @@ function lineChart(title, series, opts = {}) {
|
|
|
233
237
|
.forEach((event, eventIndex) => {
|
|
234
238
|
const [x] = point(Math.max(0, Math.min(count - 1, event.requestNumber - 1)), 0);
|
|
235
239
|
const markerY = top + 7 + (eventIndex % 3) * 9;
|
|
236
|
-
|
|
240
|
+
// data-request stays RELATIVE (tau's tooltip matches index + 1); the human
|
|
241
|
+
// description shows the GLOBAL request number (chartEvents keeps it).
|
|
242
|
+
const shown = event.globalRequestNumber ?? event.requestNumber;
|
|
243
|
+
const description = `${event.label} before request ${shown} at ${event.timestamp}`;
|
|
237
244
|
parts.push(
|
|
238
245
|
`<g class="usage-event usage-event-${esc(event.kind, true)}" data-request="${event.requestNumber}" ` +
|
|
239
246
|
`data-event-info="${esc(description, true)}" role="img" aria-label="${esc(description, true)}">` +
|
|
@@ -542,10 +549,14 @@ function eventsForRequests(events, requests) {
|
|
|
542
549
|
return events.filter((event) => event.requestNumber >= from && event.requestNumber <= to);
|
|
543
550
|
}
|
|
544
551
|
|
|
545
|
-
function chartEvents(events, requests) {
|
|
552
|
+
export function chartEvents(events, requests) {
|
|
546
553
|
if (!requests.length) return [];
|
|
547
554
|
const first = requests[0].number;
|
|
548
|
-
return events.map((event) => ({
|
|
555
|
+
return events.map((event) => ({
|
|
556
|
+
...event,
|
|
557
|
+
requestNumber: event.requestNumber - first + 1, // relative, for tau's data-request matching
|
|
558
|
+
globalRequestNumber: event.requestNumber, // absolute, for readable labels
|
|
559
|
+
}));
|
|
549
560
|
}
|
|
550
561
|
|
|
551
562
|
function renderDashboardView(requests, events, toolCalls, compactions, scopeLabel) {
|
|
@@ -756,6 +767,23 @@ for (const select of document.querySelectorAll("[data-context-segment-select]"))
|
|
|
756
767
|
select.addEventListener("change", show);
|
|
757
768
|
show();
|
|
758
769
|
}
|
|
770
|
+
// tau's tooltip hardcodes "Request " + (index + 1), which is relative inside a
|
|
771
|
+
// segment view while the x-axis shows global request numbers. Rewrite the
|
|
772
|
+
// tooltip's leading request number to the chart's global number on hover.
|
|
773
|
+
// Registered after tau's own pointermove listener, so it runs after tau writes.
|
|
774
|
+
document.querySelectorAll(".usage-chart").forEach((chart) => {
|
|
775
|
+
const nums = (chart.dataset.requestnumbers || "").split("|").map(Number);
|
|
776
|
+
chart.addEventListener("pointermove", () => {
|
|
777
|
+
const tooltip = document.querySelector(".usage-tooltip");
|
|
778
|
+
if (!tooltip) return;
|
|
779
|
+
const m = tooltip.textContent.match(/^Request (\\d+)/);
|
|
780
|
+
if (!m) return;
|
|
781
|
+
const global = nums[Number(m[1]) - 1];
|
|
782
|
+
if (global && global !== Number(m[1])) {
|
|
783
|
+
tooltip.textContent = tooltip.textContent.replace(/^Request \\d+/, "Request " + global);
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
});
|
|
759
787
|
</script>
|
|
760
788
|
</main></body></html>`;
|
|
761
789
|
}
|
|
@@ -269,7 +269,8 @@ async function run() {
|
|
|
269
269
|
check("e2e no IndexSizeError / uncaught errors in All view",
|
|
270
270
|
!allCharts.errs.some((e) => /IndexSizeError|Uncaught/i.test(e)), JSON.stringify(allCharts.errs));
|
|
271
271
|
|
|
272
|
-
// 3. latest segment view (no internal breaks): full-width tooltips
|
|
272
|
+
// 3. latest segment view (no internal breaks): full-width tooltips aligned
|
|
273
|
+
// to GLOBAL request numbers (fixture segment-4 = requests 40..45)
|
|
273
274
|
await evalJs(`window.__probe.setView("segment-4")`);
|
|
274
275
|
const seg4 = await evalJs(`
|
|
275
276
|
(() => {
|
|
@@ -281,9 +282,28 @@ async function run() {
|
|
|
281
282
|
check("e2e segment-4 tooltip works at start and end",
|
|
282
283
|
seg4.near.visible && seg4.far.visible && seg4.farReq !== seg4.nearReq,
|
|
283
284
|
JSON.stringify(seg4));
|
|
285
|
+
check("e2e segment-4 tooltip aligns to GLOBAL request numbers (40..45, not 1..6)",
|
|
286
|
+
seg4.nearReq === 40 && seg4.farReq === 45,
|
|
287
|
+
JSON.stringify(seg4));
|
|
284
288
|
check("e2e segment view has no uncaught errors",
|
|
285
289
|
!seg4.errs.some((e) => /IndexSizeError|Uncaught/i.test(e)), JSON.stringify(seg4.errs));
|
|
286
290
|
|
|
291
|
+
// 4. event markers in a segment view keep relative data-request for tau
|
|
292
|
+
// matching but describe the GLOBAL request number
|
|
293
|
+
await evalJs(`window.__probe.setView("segment-2")`);
|
|
294
|
+
const ev = await evalJs(`
|
|
295
|
+
(() => {
|
|
296
|
+
const chart = [...document.querySelectorAll("svg.usage-chart")]
|
|
297
|
+
.find((s) => s.getAttribute("aria-label") === "Prompt input by request" &&
|
|
298
|
+
s.closest(".usage-context-view") && !s.closest(".usage-context-view").hidden);
|
|
299
|
+
const g = chart.querySelector(".usage-event");
|
|
300
|
+
return g ? { dataRequest: g.getAttribute("data-request"), info: g.getAttribute("data-event-info") } : null;
|
|
301
|
+
})()
|
|
302
|
+
`);
|
|
303
|
+
check("e2e segment event marker: data-request relative, description global",
|
|
304
|
+
ev && ev.dataRequest === "1" && ev.info.includes("before request 25"),
|
|
305
|
+
JSON.stringify(ev));
|
|
306
|
+
|
|
287
307
|
cdp.close();
|
|
288
308
|
if (usesWindowsChrome && windowsPid) {
|
|
289
309
|
try { execFileSync("taskkill.exe", ["/PID", String(windowsPid), "/T", "/F"]); } catch { /* best effort */ }
|
|
@@ -4,7 +4,7 @@ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import registerCacheExport from "../index.ts";
|
|
7
|
-
import { parseEntries, analyzeStreaks, analyzeMisses, buildContextSegments, lineChartForTest, renderDashboard } from "../render.ts";
|
|
7
|
+
import { parseEntries, analyzeStreaks, analyzeMisses, buildContextSegments, lineChartForTest, renderDashboard, chartEvents } from "../render.ts";
|
|
8
8
|
|
|
9
9
|
let pass = 0, fail = 0;
|
|
10
10
|
function check(name, cond, detail = "") {
|
|
@@ -346,6 +346,23 @@ const hitOf = (r) => r.cached / (r.fresh + r.cached + r.cacheWrite);
|
|
|
346
346
|
dashboard.slice(0, 500));
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
+
// P3. segment-view request numbers align to global (tooltip override + event labels)
|
|
350
|
+
{
|
|
351
|
+
const chart = lineChartForTest("seg", [["cached", [1, 2, 3]]], { requestNumbers: [40, 41, 42] });
|
|
352
|
+
check("P3 chart carries global data-requestnumbers", chart.includes('data-requestnumbers="40|41|42"'), chart.slice(0, 220));
|
|
353
|
+
const ev = chartEvents([{ kind: "model", requestNumber: 310, label: "Model changed to b", timestamp: "12:00:00" }], [{ number: 310 }, { number: 311 }, { number: 312 }]);
|
|
354
|
+
check("P3 chartEvents keeps relative requestNumber (tau matching) and global label number",
|
|
355
|
+
ev[0].requestNumber === 1 && ev[0].globalRequestNumber === 310,
|
|
356
|
+
JSON.stringify(ev[0]));
|
|
357
|
+
const labeled = lineChartForTest("seg", [["cached", [1, 2, 3]]], {
|
|
358
|
+
requestNumbers: [310, 311, 312],
|
|
359
|
+
events: [{ kind: "model", requestNumber: 1, label: "Model changed to b", timestamp: "12:00:00", globalRequestNumber: 310 }],
|
|
360
|
+
});
|
|
361
|
+
check("P3 event description shows global request while data-request stays relative",
|
|
362
|
+
labeled.includes("before request 310 at 12:00:00") && labeled.includes('data-request="1"'),
|
|
363
|
+
labeled.slice(0, 700));
|
|
364
|
+
}
|
|
365
|
+
|
|
349
366
|
// Q. subagent results remain visible as main-session tool input -----------
|
|
350
367
|
{
|
|
351
368
|
const entries = [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maxiaochao/pi-toolkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Personal pi coding agent toolkit: /todos, /btw, /cache_export extensions, browser automation, and bundled theme/skills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
"extensions",
|
|
12
12
|
"themes",
|
|
13
|
+
"bin",
|
|
13
14
|
"README.md",
|
|
14
15
|
"CHANGELOG.md",
|
|
15
16
|
"global/AGENTS.md",
|
|
@@ -33,6 +34,9 @@
|
|
|
33
34
|
"./themes"
|
|
34
35
|
]
|
|
35
36
|
},
|
|
37
|
+
"bin": {
|
|
38
|
+
"pi-worktree": "bin/pi-worktree"
|
|
39
|
+
},
|
|
36
40
|
"peerDependencies": {
|
|
37
41
|
"@earendil-works/pi-ai": "*",
|
|
38
42
|
"@earendil-works/pi-coding-agent": "*",
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// postinstall hook: sync the bundled global AGENTS.md and seed the default theme
|
|
2
2
|
// without overriding a theme the user has already selected.
|
|
3
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
4
|
-
import { homedir } from "node:os";
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync, chmodSync } from "node:fs";
|
|
4
|
+
import { homedir, platform } from "node:os";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
|
|
8
|
+
const packageRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
|
+
|
|
8
10
|
const agentDir = join(homedir(), ".pi", "agent");
|
|
9
11
|
const bundled = readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "global", "AGENTS.md"), "utf8");
|
|
10
12
|
const agentsTarget = join(agentDir, "AGENTS.md");
|
|
@@ -41,3 +43,22 @@ if (!existsSync(settingsTarget)) {
|
|
|
41
43
|
console.warn("[pi-toolkit] could not set default theme:", error instanceof Error ? error.message : String(error));
|
|
42
44
|
}
|
|
43
45
|
}
|
|
46
|
+
|
|
47
|
+
// Install the pi-worktree CLI + bash completion on Unix-like systems.
|
|
48
|
+
// Copy (not symlink) so the running command does not depend on the repo/node_modules layout.
|
|
49
|
+
if (platform() !== "win32") {
|
|
50
|
+
try {
|
|
51
|
+
const binTarget = join(homedir(), ".local", "bin", "pi-worktree");
|
|
52
|
+
mkdirSync(dirname(binTarget), { recursive: true });
|
|
53
|
+
copyFileSync(join(packageRoot, "bin", "pi-worktree"), binTarget);
|
|
54
|
+
chmodSync(binTarget, 0o755);
|
|
55
|
+
console.log("[pi-toolkit] pi-worktree installed ->", binTarget);
|
|
56
|
+
|
|
57
|
+
const compTarget = join(homedir(), ".local", "share", "bash-completion", "completions", "pi-worktree");
|
|
58
|
+
mkdirSync(dirname(compTarget), { recursive: true });
|
|
59
|
+
copyFileSync(join(packageRoot, "bin", "pi-worktree-completion.bash"), compTarget);
|
|
60
|
+
console.log("[pi-toolkit] pi-worktree bash completion installed ->", compTarget);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.warn("[pi-toolkit] could not install pi-worktree CLI:", error instanceof Error ? error.message : String(error));
|
|
63
|
+
}
|
|
64
|
+
}
|