Skip to content

Commit c5116d4

Browse files
authored
feat/natural language session resolution (#4)
* Add session resolution substring to the skill * Remove session is modification option from UI
1 parent 6210004 commit c5116d4

15 files changed

Lines changed: 526 additions & 229 deletions

File tree

.claude/skills/askdiff-dev/SKILL.md

Lines changed: 219 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,27 +15,39 @@ proxy `/ws` to the WS server, so the UI uses the same same-origin
1515
Use this when editing `packages/server` or `packages/ui-browser` and you
1616
want changes to reload instantly instead of rebuilding/republishing.
1717

18-
> **Keep Step 1–3 in sync with `.claude/skills/askdiff/SKILL.md`.** The
19-
> diff-resolution flow (interpret → git → temp file → label) must behave
20-
> identically in both skills; only Step 4 (launch) differs. If you change
21-
> the table or the bash blocks below, change them in the user-facing
18+
> **Keep Steps 1–4 in sync with `.claude/skills/askdiff/SKILL.md`.** The
19+
> diff-resolution and session-resolution flow (interpret → git → temp file
20+
> → label → pick session) must behave identically in both skills; only
21+
> Step 5 (launch) differs. If you change any block below — including the
22+
> session-resolution logic in Step 4 — change it in the user-facing
2223
> `askdiff` skill too.
2324
24-
## Step 1 — figure out which diff the user wants
25+
## Step 1 — figure out which diff the user wants (and which session)
2526

2627
Look at the message that invoked this skill. Anything after `/askdiff-dev`
27-
is the user's diff description (may be empty).
28+
is free-form natural language that may carry **two** kinds of information:
2829

29-
| User said | git command | Suggested label |
30+
1. A **diff description** — what to diff (handled by the table/ladder
31+
below). This part is what Step 2 turns into a `git diff` command.
32+
2. An optional **session hint** — which Claude session to attach to
33+
(handled by the *Session hint* subsection at the end of this step,
34+
then resolved in Step 4).
35+
36+
Either or both may be empty. The diff-description part may be empty
37+
(working tree); the session hint defaults to "the invoking session" when
38+
absent. Treat them independently — first identify and set aside the
39+
session hint, then pass the rest to the diff resolution below.
40+
41+
| `diff_description` | git command | Suggested label |
3042
|---|---|---|
31-
| `/askdiff-dev` (no args) | working tree — see Step 2 | `Working tree` |
32-
| `/askdiff-dev last commit` | `git diff HEAD~1 HEAD` | `HEAD~1..HEAD` |
33-
| `/askdiff-dev last 3 commits` | `git diff HEAD~3 HEAD` | `HEAD~3..HEAD` |
34-
| `/askdiff-dev the 5th latest commit` | `git diff HEAD~5 HEAD~4` | `HEAD~5..HEAD~4` |
35-
| `/askdiff-dev current branch against feature/test` | `git diff feature/test...HEAD` (three-dot, PR semantics) | `feature/test…HEAD` |
36-
| `/askdiff-dev main vs my branch` | `git diff main...HEAD` | `main…HEAD` |
37-
| `/askdiff-dev abc123 vs def456` | `git diff abc123 def456` | `abc123..def456` |
38-
| `/askdiff-dev staged` | `git diff --cached` | `staged` |
43+
| (empty) | working tree — see Step 2 | `Working tree` |
44+
| `last commit` | `git diff HEAD~1 HEAD` | `HEAD~1..HEAD` |
45+
| `last 3 commits` | `git diff HEAD~3 HEAD` | `HEAD~3..HEAD` |
46+
| `the 5th latest commit` | `git diff HEAD~5 HEAD~4` | `HEAD~5..HEAD~4` |
47+
| `current branch against feature/test` | `git diff feature/test...HEAD` (three-dot, PR semantics) | `feature/test…HEAD` |
48+
| `main vs my branch` | `git diff main...HEAD` | `main…HEAD` |
49+
| `abc123 vs def456` | `git diff abc123 def456` | `abc123..def456` |
50+
| `staged` | `git diff --cached` | `staged` |
3951

4052
Defaults when the user is ambiguous:
4153
- "branch X against branch Y" / "X vs Y" between two named refs ⇒ three-dot
@@ -104,6 +116,48 @@ each ref the user named directly. If any fails, stop and tell the user
104116
which ref didn't resolve — do not launch the server. (Refs returned by the
105117
search ladder are already validated by virtue of `git log` finding them.)
106118

119+
### Session hint (optional)
120+
121+
By default `/askdiff-dev` attaches the WS server to the **invoking**
122+
session (the one running this skill). The user may override that by
123+
carrying a phrase about the target session in their input. Decompose the
124+
input into two parts:
125+
126+
- `diff_description` — what to diff (everything Step 1's table/ladder uses)
127+
- `session_hint` — one of `none`, `explicit-id <uuid-or-prefix>`, or
128+
`keywords <a, b, c, …>`
129+
130+
**Trigger phrases** for `session_hint` (illustrative — generalize from
131+
these):
132+
133+
- "attached to (the/a) session …"
134+
- "connected to (the/a) session/conversation/chat …"
135+
- "in (the/a/our) session [about / where / that] …"
136+
- "from (the/a) [chat / conversation] [where / about] …"
137+
- "the session in which …", "session that …"
138+
- "session id `<uuid>`", "session `<uuid>`", or a bare UUID-shaped token
139+
(8+ hex chars, optionally with dashes)
140+
141+
**Examples:**
142+
143+
| User input | `diff_description` | `session_hint` |
144+
|---|---|---|
145+
| `last commit` | `last commit` | none |
146+
| (empty) | (working tree) | none |
147+
| `the staleness commit attached to the session where we discussed mtime checks` | `the staleness commit` | keywords: "mtime checks" |
148+
| `last commit in our session about pricing rules and tax math` | `last commit` | keywords: "pricing rules", "tax math" |
149+
| `session 322bc90a` | (working tree) | explicit-id: `322bc90a` |
150+
| `abc123 vs def456 in session 322bc90a-714f-41b7-914e-109404e46072` | `abc123 vs def456` | explicit-id: full UUID |
151+
152+
**Be conservative.** If parsing is itself ambiguous (e.g. "the foo session"
153+
— is "session" a noun in the diff or a trigger?), treat the whole input
154+
as `diff_description` (no session hint). Don't ask the user to clarify the
155+
parse — just resolve the diff and proceed; the default attachment to the
156+
invoking session is always safe.
157+
158+
The session hint is consumed in Step 4. Steps 2 and 3 use only
159+
`diff_description`.
160+
107161
## Step 2 — write the diff to a session-stable file
108162

109163
First resolve the parent Claude Code session and project cwd. All `/tmp`
@@ -160,15 +214,155 @@ show "No changes."
160214
**Mark the diff as volatile if you took the working-tree path.** Set
161215
`volatile=1` if Step 2 used the working-tree block (the diff can drift as
162216
the user keeps editing); set `volatile=0` for description-based diffs
163-
(immutable git history). Step 4 forwards this to the server as
217+
(immutable git history). Step 5 forwards this to the server as
164218
`ASKDIFF_DIFF_VOLATILE`, which gates the per-file mtime staleness check.
165219

166220
## Step 3 — pick a short label
167221

168222
Use the "Suggested label" column above. For the working-tree case, use
169223
`Working tree`. Keep it under ~40 chars. This becomes `ASKDIFF_DIFF_LABEL`.
170224

171-
## Step 4 — launch (in-repo)
225+
## Step 4 — resolve the target session
226+
227+
Compute `attached_session` and `session_source` from `session_hint`
228+
(captured in Step 1). The default is the invoking session — that path
229+
matches today's behavior and skips all the matching logic below.
230+
231+
```bash
232+
attached_session="$session_id" # default = invoking
233+
session_source="invoking"
234+
235+
sessions_dir="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/projects/$(echo "$project_cwd" | tr '/' '-')"
236+
```
237+
238+
### 4a. No hint → invoking session (default)
239+
240+
If `session_hint` is `none`, leave the defaults and skip to Step 5.
241+
242+
### 4b. Explicit ID → resolve
243+
244+
If `session_hint` is `explicit-id <X>`:
245+
246+
```bash
247+
explicit_id="<X>"
248+
249+
if echo "$explicit_id" | grep -qE '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'; then
250+
# Full UUID: trust it if the file exists.
251+
if [ -f "$sessions_dir/$explicit_id.jsonl" ]; then
252+
attached_session="$explicit_id"
253+
session_source="explicit"
254+
else
255+
# → AskUserQuestion: session not found, use current?
256+
:
257+
fi
258+
else
259+
# Short prefix: glob and disambiguate.
260+
shopt -s nullglob
261+
matches=( "$sessions_dir/${explicit_id}"*.jsonl )
262+
shopt -u nullglob
263+
case ${#matches[@]} in
264+
1)
265+
attached_session=$(basename "${matches[0]}" .jsonl)
266+
session_source="explicit"
267+
;;
268+
0)
269+
# → AskUserQuestion: no session matches "<prefix>", use current?
270+
: ;;
271+
*)
272+
# → AskUserQuestion: pick one of N candidates (list short-uuid · age)
273+
: ;;
274+
esac
275+
fi
276+
```
277+
278+
For the AskUserQuestion branches above:
279+
280+
- **Not found / 0 matches**: options are "Use current session" and "Cancel" (do not launch).
281+
- **Multiple prefix matches**: one option per UUID labelled `<short-uuid> · <age>` (compute age below), plus "Use current session". Set `attached_session` and `session_source="explicit"` from the user's pick.
282+
283+
### 4c. Keywords → grep, decide, possibly ask
284+
285+
If `session_hint` is `keywords <a, b, c, …>`:
286+
287+
```bash
288+
needles_file=$(mktemp)
289+
290+
# 1. The user's session keywords (literal phrases — one per line).
291+
printf '%s\n' "<keyword 1>" "<keyword 2>" >> "$needles_file"
292+
293+
# 2. Changed file paths from the resolved diff (additional signal,
294+
# catches sessions that Read/Edit/Write'd those files).
295+
command grep -E '^\+\+\+ b/' "$diff_file" | sed -E 's|^\+\+\+ b/||' >> "$needles_file"
296+
297+
# 3. Commit SHAs (only for description-based diffs — Claude knows these
298+
# from Step 1's resolution). Skip for working-tree diffs.
299+
for sha in "<sha1>" "<sha2>"; do
300+
[ -n "$sha" ] && printf '%s\n' "$sha" >> "$needles_file"
301+
done
302+
303+
# 4. Branch names (only for the X...Y / X..Y form).
304+
for br in "<branch1>" "<branch2>"; do
305+
[ -n "$br" ] && printf '%s\n' "$br" >> "$needles_file"
306+
done
307+
308+
# Search recent JSONLs (mtime −30d), filter out the invoking session
309+
# (it always matches its own JSONL because the user just typed the
310+
# keywords into it), return at most 5 rows of "<count> <uuid>" sorted
311+
# by hit count desc.
312+
#
313+
# Three subtle things below — change them at your peril:
314+
# - `command grep` bypasses any shell function/alias that wraps grep.
315+
# Claude Code's harness wraps grep as a function that proxies to
316+
# ugrep with extra flags, and that wrapper breaks `-Ff <patternfile>`.
317+
# - We pipe `find` directly into `while read`, instead of `for f in
318+
# $(find ...)`. zsh doesn't word-split unquoted variable expansions
319+
# on newlines by default; that for-loop iterates exactly ONCE with
320+
# $f containing every path concatenated.
321+
# - `count=$(grep -c ...)` then `[ -z "$count" ] && count=0` — do NOT
322+
# write `count=$(grep -c ... || echo 0)`. grep -c always prints a
323+
# number (0 on no match) AND exits non-zero when there are no
324+
# matches, so the `|| echo 0` doubles the output to "0\n0" and
325+
# breaks the numeric `-gt` comparison.
326+
results=$(
327+
find "$sessions_dir" -name '*.jsonl' -mtime -30 -type f 2>/dev/null \
328+
| while read -r f; do
329+
uuid=$(basename "$f" .jsonl)
330+
[ "$uuid" = "$session_id" ] && continue
331+
count=$(command grep -cFf "$needles_file" "$f" 2>/dev/null)
332+
[ -z "$count" ] && count=0
333+
[ "$count" -gt 0 ] && echo "$count $uuid"
334+
done | sort -rn | head -5
335+
)
336+
rm -f "$needles_file"
337+
```
338+
339+
Read `$results` and route:
340+
341+
| Result shape | Action |
342+
|---|---|
343+
| 0 lines | AskUserQuestion: "no session matched `<keywords>`. Use current session?" → "Use current" or "Cancel and refine" |
344+
| 1 line | use that UUID; `attached_session=$uuid`, `session_source="matched"` |
345+
| 2+ lines, top count ≥ 2× second | use top-1; `session_source="matched"` |
346+
| 2–5 lines, comparable counts | AskUserQuestion: list each candidate as `<short-uuid> · <age> · <count> hits`, plus "Use current session" |
347+
348+
For ages (used in AskUserQuestion labels):
349+
350+
```bash
351+
now=$(date +%s)
352+
mtime=$(stat -f %m "$sessions_dir/$uuid.jsonl" 2>/dev/null || stat -c %Y "$sessions_dir/$uuid.jsonl")
353+
age_sec=$(( now - mtime ))
354+
if [ $age_sec -lt 86400 ]; then
355+
age_str="$((age_sec / 3600))h ago"
356+
else
357+
age_str="$((age_sec / 86400))d ago"
358+
fi
359+
```
360+
361+
**Don't widen the search automatically.** If results are empty or
362+
unclear, surface that to the user via AskUserQuestion. Re-run with
363+
broader scope (e.g. `mtime -90`) only if the user explicitly says to.
364+
365+
## Step 5 — launch (in-repo)
172366

173367
Run as a single Bash command so the discovered values survive into the
174368
launch. Substitute `EXTRA_DIFF_FILE` and `EXTRA_DIFF_LABEL` literally with
@@ -220,7 +414,7 @@ fi
220414
# 3. Start the WS server (in-repo via tsx).
221415
cd "$project_cwd" \
222416
&& PORT=$port \
223-
ASKDIFF_SESSION_ID="$session_id" \
417+
ASKDIFF_SESSION_ID="$attached_session" \
224418
ASKDIFF_PROJECT_CWD="$project_cwd" \
225419
ASKDIFF_DIFF_FILE="$EXTRA_DIFF_FILE" \
226420
ASKDIFF_DIFF_LABEL="$EXTRA_DIFF_LABEL" \
@@ -250,8 +444,9 @@ if ! $ui_running; then
250444
fi
251445
252446
# 5. Wait for Vite to print its "Local: http://localhost:XXXX/" line.
447+
# (`command grep` bypasses the harness's grep wrapper — see Step 4c.)
253448
for _ in $(seq 1 60); do
254-
grep -q "Local:" "$ui_log" 2>/dev/null && break
449+
command grep -q "Local:" "$ui_log" 2>/dev/null && break
255450
sleep 0.25
256451
done
257452
@@ -279,7 +474,11 @@ echo "WS PID: $new_pid (saved to $pid_file)"
279474

280475
Then tell the user:
281476
- the WS server port (visible in the `listening on ws://...` line)
282-
- the resolved Claude session ID (from the `claude session:` line)
477+
- the resolved Claude session ID (from the `claude session:` line) — and
478+
if `$session_source` is `explicit` or `matched`, say so explicitly
479+
(e.g. "attached to matched session 322bc90a (was: invoking)") so the
480+
user knows their asks are not landing in the current session's
481+
transcript
283482
- the diff label (always set)
284483
- the WS log file (printed as the `WS log:` line — `/tmp/askdiff.<suffix>.log`)
285484
- the Vite log file (printed as the `UI log:` line — `/tmp/askdiff-ui.<suffix>.log`)

0 commit comments

Comments
 (0)