@hayasaka7/haya-pet 0.3.1 → 0.3.3
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 +19 -0
- package/README.md +1 -1
- package/apps/companion/src/renderer/session-bubbles.js +14 -9
- package/apps/companion/src/renderer/styles.css +1 -0
- package/apps/companion/test/session-bubbles.test.mjs +28 -0
- package/docs/known-issues.md +17 -2
- package/docs/troubleshooting.md +1 -0
- package/package.json +1 -1
- package/packages/adapters/src/claude-hooks.js +1 -2
- package/packages/adapters/test/claude-hooks.test.mjs +6 -2
- package/packages/session-core/src/bubble-view.js +2 -1
- package/packages/session-core/src/summaries.js +14 -0
- package/packages/session-core/test/bubble-view.test.mjs +8 -0
- package/packages/session-core/test/summaries.test.mjs +24 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,25 @@ All notable changes to HAYA Pet are documented here. This project adheres to
|
|
|
7
7
|
> 0.2.0 npm publish; they are listed under 0.2.1, which is the first version that
|
|
8
8
|
> ships them.
|
|
9
9
|
|
|
10
|
+
## [0.3.3]
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Claude Code subagent completion no longer changes the main session status.**
|
|
14
|
+
Claude Code can emit `SubagentStop` after the main agent has already stopped,
|
|
15
|
+
so treating that event as `idle` could make the pet react to a stale subagent
|
|
16
|
+
completion instead of the main agent's real state. The Claude hook adapter now
|
|
17
|
+
ignores `SubagentStop`; the main turn still ends on Claude's `Stop` event.
|
|
18
|
+
|
|
19
|
+
## [0.3.2]
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- **Session bubble titles no longer run off the screen.** A long project name
|
|
23
|
+
used to stretch the bubble out to the panel's full width. The title now keeps
|
|
24
|
+
the **client name in full** (Codex, Claude Code, …) and shows the **project
|
|
25
|
+
name capped at 10 characters** with an ellipsis when it's longer (e.g.
|
|
26
|
+
`netdisk-server` → `netdisk-se...`); the complete `Client · Project` is kept
|
|
27
|
+
as a hover tooltip so nothing is lost.
|
|
28
|
+
|
|
10
29
|
## [0.3.1]
|
|
11
30
|
|
|
12
31
|
### Fixed
|
package/README.md
CHANGED
|
@@ -218,6 +218,15 @@ function renderBubble(bubble) {
|
|
|
218
218
|
const title = document.createElement("div");
|
|
219
219
|
title.className = "title";
|
|
220
220
|
|
|
221
|
+
// Persistent title parts (mutated in place across updates, like everything
|
|
222
|
+
// else here). The client name is always shown in full; the project name is
|
|
223
|
+
// shown compact (bubble.projectLabel) with the full name kept as a tooltip.
|
|
224
|
+
const client = document.createElement("span");
|
|
225
|
+
client.className = "client";
|
|
226
|
+
const project = document.createElement("span");
|
|
227
|
+
project.className = "project";
|
|
228
|
+
title.append(client, project);
|
|
229
|
+
|
|
221
230
|
const activity = document.createElement("div");
|
|
222
231
|
activity.className = "activity";
|
|
223
232
|
|
|
@@ -239,8 +248,11 @@ function applyBubble(el, bubble) {
|
|
|
239
248
|
icon.title = bubble.statusLabel;
|
|
240
249
|
|
|
241
250
|
const [title, activity] = body.children;
|
|
242
|
-
|
|
243
|
-
|
|
251
|
+
const [client, project] = title.children;
|
|
252
|
+
client.textContent = bubble.clientName;
|
|
253
|
+
project.textContent = bubble.projectLabel ?? bubble.projectName;
|
|
254
|
+
// Hover reveals the full, untruncated "Client · Project".
|
|
255
|
+
title.title = bubble.projectName ? `${bubble.clientName} · ${bubble.projectName}` : bubble.clientName;
|
|
244
256
|
activity.textContent = bubble.summary;
|
|
245
257
|
activity.title = `${bubble.statusLabel} · ${bubble.elapsedLabel}`;
|
|
246
258
|
}
|
|
@@ -257,10 +269,3 @@ function mostUrgentKind(bubbles) {
|
|
|
257
269
|
}
|
|
258
270
|
return best;
|
|
259
271
|
}
|
|
260
|
-
|
|
261
|
-
function escapeHtml(value) {
|
|
262
|
-
return String(value ?? "")
|
|
263
|
-
.replace(/&/g, "&")
|
|
264
|
-
.replace(/</g, "<")
|
|
265
|
-
.replace(/>/g, ">");
|
|
266
|
-
}
|
|
@@ -170,6 +170,34 @@ test("scrolls to a session when it newly fails", () => {
|
|
|
170
170
|
}
|
|
171
171
|
});
|
|
172
172
|
|
|
173
|
+
test("shows the compact project label and keeps the full name as a tooltip", () => {
|
|
174
|
+
const restoreDocument = installFakeDocument();
|
|
175
|
+
try {
|
|
176
|
+
const container = new FakeElement("div");
|
|
177
|
+
const listView = createBubbleList(container, { onRender: createHostOnRender(container) });
|
|
178
|
+
|
|
179
|
+
listView.render([{
|
|
180
|
+
sessionId: "s1",
|
|
181
|
+
statusKind: "working",
|
|
182
|
+
statusLabel: "Working",
|
|
183
|
+
clientName: "Claude Code",
|
|
184
|
+
projectName: "netdisk-server",
|
|
185
|
+
projectLabel: "netdisk-se...",
|
|
186
|
+
summary: "running",
|
|
187
|
+
elapsedLabel: "1s"
|
|
188
|
+
}]);
|
|
189
|
+
|
|
190
|
+
const bubble = findBubble(container, "s1");
|
|
191
|
+
const title = childByClass(childByClass(bubble, "body"), "title");
|
|
192
|
+
assert.equal(childByClass(title, "client").textContent, "Claude Code");
|
|
193
|
+
assert.equal(childByClass(title, "project").textContent, "netdisk-se...");
|
|
194
|
+
// The full, untruncated name stays reachable on hover.
|
|
195
|
+
assert.equal(title.title, "Claude Code · netdisk-server");
|
|
196
|
+
} finally {
|
|
197
|
+
restoreDocument();
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
173
201
|
test("clears everything when no sessions remain", () => {
|
|
174
202
|
const restoreDocument = installFakeDocument();
|
|
175
203
|
try {
|
package/docs/known-issues.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
Issues found in live use, with their current status.
|
|
4
4
|
|
|
5
|
+
## ✅ Resolved: Claude Code subagent completion changed the main session status
|
|
6
|
+
|
|
7
|
+
- **Symptom:** In Claude Code multi-agent runs, the main agent could already be
|
|
8
|
+
stopped while a subagent was still finishing. When that late subagent emitted
|
|
9
|
+
`SubagentStop`, the pet treated it as a main-session `idle` update and could
|
|
10
|
+
show a misleading working/done transition after the main agent had settled.
|
|
11
|
+
- **Root cause:** The Claude hook table mapped `SubagentStop` to `idle`. That is
|
|
12
|
+
only safe if subagent completion is ordered before the main turn finishes, which
|
|
13
|
+
Claude Code does not guarantee.
|
|
14
|
+
- **Fix:** Claude `SubagentStop` is now ignored. Main-session idle still comes
|
|
15
|
+
from Claude's real `Stop` hook, while late subagent completion cannot override
|
|
16
|
+
the current main-agent state. Codex keeps its separate behavior because Codex
|
|
17
|
+
uses `Stop` as the only idle signal and treats `SubagentStop` as mid-turn.
|
|
18
|
+
|
|
5
19
|
## ✅ Resolved: false "waiting for approval" while Codex auto-reviews an approval (Approve for me)
|
|
6
20
|
|
|
7
21
|
- **Symptom:** Running Codex under the pet with the **"Approve for me"** preset
|
|
@@ -228,8 +242,9 @@ observation (`--observe`) or L1 lifecycle as the fallback. Current state:
|
|
|
228
242
|
lifecycle status). Live in-session status is **opt-in** via `HAYA_PET_HOOKS=1`,
|
|
229
243
|
which injects a settings file (`claude --settings <stable-file>`, no change to
|
|
230
244
|
your global config) wiring Claude's `UserPromptSubmit`/`PreToolUse`/`PostToolUse`/
|
|
231
|
-
`Notification`/`PreCompact`/`Stop
|
|
232
|
-
|
|
245
|
+
`Notification`/`PreCompact`/`Stop` events to `haya-pet state <state>`, reported
|
|
246
|
+
to the daemon over the IPC pipe. `SubagentStop` is intentionally ignored because
|
|
247
|
+
it is not a main-turn idle signal. `PreToolUse` distinguishes
|
|
233
248
|
file-editing tools (`Edit`/`Write`/`MultiEdit`/`NotebookEdit` → *editing files*)
|
|
234
249
|
from other tools (→ *running tools*) via the hook `matcher`. **Why opt-in:**
|
|
235
250
|
injecting hooks makes Claude show a one-time *review hooks* trust prompt; the
|
package/docs/troubleshooting.md
CHANGED
|
@@ -16,6 +16,7 @@ deferred problems with known root causes.
|
|
|
16
16
|
| Terminal scroll / Shift+Tab / backspace odd while a CLI runs under `haya-pet run` | Fixed — `haya-pet run` now uses native passthrough by default (full fidelity). If you opted into `--observe`, drop it. See [known-issues.md](known-issues.md). |
|
|
17
17
|
| Pet shows only **idle/lifecycle** while **Claude Code** works | Live in-session status is opt-in: run `haya-pet hooks on` once (persisted). The first `haya-pet run` afterward shows a one-time Claude *review hooks* prompt — approve it. Also make sure the companion is running (`haya-pet start`). Check the toggle with `haya-pet hooks status`. |
|
|
18
18
|
| Typing doesn't work / **Claude Code** TUI frozen under `haya-pet run` | You have hooks enabled and Claude is showing its *review hooks* trust prompt (approve it once), or your Claude is too old for `--settings`. Run `haya-pet hooks off` (or set `HAYA_PET_NO_HOOKS=1`) for native passthrough with lifecycle-only status — typing and Shift+Tab work normally. |
|
|
19
|
+
| Pet changes status after a **Claude Code** subagent finishes, even though the main agent already stopped | Fixed — Claude `SubagentStop` is ignored because it is not a reliable main-turn state. Update to the latest version and restart the wrapped Claude session so the new hook settings are used. |
|
|
19
20
|
| Pet shows only **idle/lifecycle** while **Codex** works | Live status is opt-in: run `haya-pet hooks on` once (persisted, global), then `haya-pet run --client codex -- codex`; approve Codex's one-time *review hooks* prompt. `thinking`/`idle` come from hooks, `running_tool`/`editing_files` from a transcript watcher, and approval states from the `PermissionRequest` hook plus a guardian-review watcher. |
|
|
20
21
|
| Pet showed **waiting for approval** while **Codex** auto-reviewed the request ("Approve for me") | Fixed — with `approvals_reviewer = auto_review` (legacy `guardian_subagent`) Codex's guardian decides without asking you; the pet now shows **reviewing** during the assessment, then **working** on an allow verdict or **thinking** on a deny. *Waiting for approval* still shows when Codex actually asks you (`approvals_reviewer = "user"`). |
|
|
21
22
|
| **Codex** live status didn't turn on / you pass your own `-p`/`--profile` | Codex allows only one profile, so haya-pet skips hook injection when you supply your own and prints a notice. Drop your `-p` for that run to get live status, or accept lifecycle-only. |
|
package/package.json
CHANGED
|
@@ -34,8 +34,7 @@ const HOOK_TABLE = Object.freeze([
|
|
|
34
34
|
{ event: "PermissionDenied", state: "idle", summary: "denied" },
|
|
35
35
|
{ event: "PreCompact", state: "compacting" },
|
|
36
36
|
{ event: "Stop", state: "idle" },
|
|
37
|
-
{ event: "StopFailure", state: "idle", summary: "stopped" }
|
|
38
|
-
{ event: "SubagentStop", state: "idle" }
|
|
37
|
+
{ event: "StopFailure", state: "idle", summary: "stopped" }
|
|
39
38
|
]);
|
|
40
39
|
|
|
41
40
|
// Resolve the pet state for a Claude event. `detail` is the tool name for
|
|
@@ -9,12 +9,15 @@ test("mapClaudeEventToState covers activity events", () => {
|
|
|
9
9
|
assert.equal(mapClaudeEventToState("PreCompact"), "compacting");
|
|
10
10
|
assert.equal(mapClaudeEventToState("Stop"), "idle");
|
|
11
11
|
assert.equal(mapClaudeEventToState("StopFailure"), "idle");
|
|
12
|
-
assert.equal(mapClaudeEventToState("SubagentStop"), "idle");
|
|
13
12
|
assert.equal(mapClaudeEventToState("PermissionDenied"), "idle");
|
|
14
13
|
assert.equal(mapClaudeEventToState("PermissionRequest"), "waiting_approval");
|
|
15
14
|
assert.equal(mapClaudeEventToState("Unknown"), undefined);
|
|
16
15
|
});
|
|
17
16
|
|
|
17
|
+
test("mapClaudeEventToState ignores Claude SubagentStop", () => {
|
|
18
|
+
assert.equal(mapClaudeEventToState("SubagentStop"), undefined);
|
|
19
|
+
});
|
|
20
|
+
|
|
18
21
|
test("mapClaudeEventToState branches PreToolUse on tool name", () => {
|
|
19
22
|
assert.equal(mapClaudeEventToState("PreToolUse", "Bash"), "running_tool");
|
|
20
23
|
assert.equal(mapClaudeEventToState("PreToolUse", "Edit"), "editing_files");
|
|
@@ -80,8 +83,9 @@ test("buildClaudeHookSettings includes all subscribed events", () => {
|
|
|
80
83
|
for (const event of [
|
|
81
84
|
"UserPromptSubmit", "PreToolUse", "PostToolUse", "PostToolUseFailure",
|
|
82
85
|
"PermissionRequest", "Notification", "PermissionDenied", "PreCompact",
|
|
83
|
-
"Stop", "StopFailure"
|
|
86
|
+
"Stop", "StopFailure"
|
|
84
87
|
]) {
|
|
85
88
|
assert.ok(settings.hooks[event], `missing hook event ${event}`);
|
|
86
89
|
}
|
|
90
|
+
assert.equal(settings.hooks.SubagentStop, undefined);
|
|
87
91
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { mapAiStateToPetAction } from "../../pet-core/src/atlas.js";
|
|
2
|
-
import { buildSessionSummary, buildStatusLabel, formatElapsed } from "./summaries.js";
|
|
2
|
+
import { buildSessionSummary, buildStatusLabel, formatElapsed, truncateProjectName } from "./summaries.js";
|
|
3
3
|
|
|
4
4
|
// Collapses the full AI-state vocabulary into the four progress kinds the
|
|
5
5
|
// bubble panel renders: a spinning "working" circle, a "done" check mark (held
|
|
@@ -36,6 +36,7 @@ export function buildBubbleView(session, now = Date.now(), options = {}) {
|
|
|
36
36
|
clientId: session.clientId,
|
|
37
37
|
clientName: session.clientDisplayName ?? session.clientId,
|
|
38
38
|
projectName: session.projectName,
|
|
39
|
+
projectLabel: truncateProjectName(session.projectName),
|
|
39
40
|
state: session.state,
|
|
40
41
|
statusLabel: buildStatusLabel(session.state),
|
|
41
42
|
statusKind: resolveBubbleStatusKind(session.state),
|
|
@@ -26,6 +26,20 @@ export function buildSessionSummary(session) {
|
|
|
26
26
|
return buildStatusLabel(session?.state);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
// Compacts a project name for the session bubble title, which sits beside the
|
|
30
|
+
// (always-full) client name in a narrow overlay. Names up to `maxLength`
|
|
31
|
+
// characters show whole; longer ones are cut to `maxLength` and marked with an
|
|
32
|
+
// ellipsis. The full name is kept elsewhere on the view model for a tooltip.
|
|
33
|
+
const DEFAULT_PROJECT_NAME_LENGTH = 10;
|
|
34
|
+
|
|
35
|
+
export function truncateProjectName(name, maxLength = DEFAULT_PROJECT_NAME_LENGTH) {
|
|
36
|
+
const text = typeof name === "string" ? name : "";
|
|
37
|
+
if (text.length <= maxLength) {
|
|
38
|
+
return text;
|
|
39
|
+
}
|
|
40
|
+
return `${text.slice(0, maxLength)}...`;
|
|
41
|
+
}
|
|
42
|
+
|
|
29
43
|
export function formatElapsed(ms) {
|
|
30
44
|
const totalSeconds = Math.max(0, Math.floor((Number.isFinite(ms) ? ms : 0) / 1000));
|
|
31
45
|
|
|
@@ -20,6 +20,8 @@ test("builds a bubble view model with label, summary, action, and elapsed", () =
|
|
|
20
20
|
assert.equal(view.clientId, "codex");
|
|
21
21
|
assert.equal(view.clientName, "Codex");
|
|
22
22
|
assert.equal(view.projectName, "netdisk-server");
|
|
23
|
+
// The full name is preserved; projectLabel is the compact bubble display form.
|
|
24
|
+
assert.equal(view.projectLabel, "netdisk-se...");
|
|
23
25
|
assert.equal(view.state, "waiting_approval");
|
|
24
26
|
assert.equal(view.statusLabel, "Waiting for approval");
|
|
25
27
|
assert.equal(view.summary, "waiting for command approval");
|
|
@@ -64,6 +66,12 @@ test("breaks connect-time ties by session id for a deterministic order", () => {
|
|
|
64
66
|
assert.deepEqual(views.map((view) => view.sessionId), ["sess_a", "sess_b"]);
|
|
65
67
|
});
|
|
66
68
|
|
|
69
|
+
test("keeps a short project name whole in projectLabel", () => {
|
|
70
|
+
const view = buildBubbleView({ ...baseSession, projectName: "api" }, 6_000);
|
|
71
|
+
assert.equal(view.projectName, "api");
|
|
72
|
+
assert.equal(view.projectLabel, "api");
|
|
73
|
+
});
|
|
74
|
+
|
|
67
75
|
test("marks the selected/pinned session", () => {
|
|
68
76
|
const views = buildBubbleViews([baseSession], 6_000, { selectedSessionId: "sess_a" });
|
|
69
77
|
assert.equal(views[0].selected, true);
|
|
@@ -3,7 +3,8 @@ import { test } from "../../../test/harness.mjs";
|
|
|
3
3
|
import {
|
|
4
4
|
buildStatusLabel,
|
|
5
5
|
buildSessionSummary,
|
|
6
|
-
formatElapsed
|
|
6
|
+
formatElapsed,
|
|
7
|
+
truncateProjectName
|
|
7
8
|
} from "../src/summaries.js";
|
|
8
9
|
|
|
9
10
|
test("maps every normalized state to a human status label", () => {
|
|
@@ -36,3 +37,25 @@ test("formats elapsed durations compactly", () => {
|
|
|
36
37
|
assert.equal(formatElapsed(65_000), "1m 5s");
|
|
37
38
|
assert.equal(formatElapsed(3_725_000), "1h 2m");
|
|
38
39
|
});
|
|
40
|
+
|
|
41
|
+
test("keeps short project names whole", () => {
|
|
42
|
+
assert.equal(truncateProjectName("api"), "api");
|
|
43
|
+
// Exactly at the 10-character budget stays untouched (no ellipsis).
|
|
44
|
+
assert.equal(truncateProjectName("ten-charrr"), "ten-charrr");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("truncates long project names to 10 characters plus an ellipsis", () => {
|
|
48
|
+
assert.equal(truncateProjectName("netdisk-server"), "netdisk-se...");
|
|
49
|
+
assert.equal(truncateProjectName("a-very-long-project-name"), "a-very-lon...");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("honours a custom max length", () => {
|
|
53
|
+
assert.equal(truncateProjectName("netdisk-server", 4), "netd...");
|
|
54
|
+
assert.equal(truncateProjectName("abc", 4), "abc");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("coerces missing or non-string project names to an empty string", () => {
|
|
58
|
+
assert.equal(truncateProjectName(undefined), "");
|
|
59
|
+
assert.equal(truncateProjectName(null), "");
|
|
60
|
+
assert.equal(truncateProjectName(123), "");
|
|
61
|
+
});
|