@hayasaka7/haya-pet 0.3.6 → 0.3.7
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 +14 -0
- package/README.md +3 -3
- package/apps/companion/src/main/tray-menu.js +2 -28
- package/apps/companion/test/tray-menu.test.mjs +3 -14
- package/docs/known-issues.md +14 -0
- package/docs/screenshots/README.md +1 -1
- package/docs/troubleshooting.md +1 -0
- package/package.json +1 -1
- package/packages/cli-core/src/codex-hook-injection.js +51 -2
- package/packages/cli-core/test/codex-hook-injection.test.mjs +30 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,14 @@ 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.7]
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- **The tray menu no longer shows state-only controls.** Hidden **Display Mode**
|
|
14
|
+
and **Attach Bubbles to Terminals** until those settings have real runtime
|
|
15
|
+
behavior. **Active Sessions** stays visible while session actions continue in a
|
|
16
|
+
separate workflow.
|
|
17
|
+
|
|
10
18
|
## [0.3.6]
|
|
11
19
|
|
|
12
20
|
### Fixed
|
|
@@ -38,6 +46,12 @@ All notable changes to HAYA Pet are documented here. This project adheres to
|
|
|
38
46
|
watcher, so a resumed main rollout could be rejected before the guardian trunk
|
|
39
47
|
was matched to it. The guardian watcher now uses the same fresh-mtime + wrapped
|
|
40
48
|
cwd rule for resumed main sessions before following the guardian review trunk.
|
|
49
|
+
- **Codex hook review is one-time again.** Codex stores approved hook hashes in
|
|
50
|
+
the generated `$CODEX_HOME/haya-pet.config.toml` profile under `[hooks.state]`.
|
|
51
|
+
The injector used to rewrite the whole managed profile on every launch, deleting
|
|
52
|
+
that trust state and forcing Codex to ask for hook review every time. The
|
|
53
|
+
injector now preserves Codex's hook trust tables while regenerating the HAYA
|
|
54
|
+
hook definitions.
|
|
41
55
|
|
|
42
56
|
### Added
|
|
43
57
|
- **`HAYA_PET_DAEMON_DEBUG` diagnostic.** When set to a file path, the companion
|
package/README.md
CHANGED
|
@@ -136,7 +136,7 @@ success or failure briefly, then fades.
|
|
|
136
136
|
| Global pet | Reacts to the highest-priority session and can be dragged anywhere. |
|
|
137
137
|
| Session bubbles | One bubble per running AI session, ordered by connect time. |
|
|
138
138
|
| Folder button | Folds the bubbles away when you want a cleaner desktop. |
|
|
139
|
-
| Tray menu | Show/hide,
|
|
139
|
+
| Tray menu | Show/hide, active sessions, installed pets, reset position, updates, and quit. |
|
|
140
140
|
| Resize grip | Hover the pet, drag the corner, and keep the size you like. |
|
|
141
141
|
|
|
142
142
|
## Screenshots
|
|
@@ -144,7 +144,7 @@ success or failure briefly, then fades.
|
|
|
144
144
|
| | |
|
|
145
145
|
|---|---|
|
|
146
146
|
| **The global pet** - reacting to the highest-priority session.<br> | **Session bubbles** - one per active session, with status icons.<br> |
|
|
147
|
-
| **Folder collapsed** - bubbles tucked away beside the pet.<br> | **Tray menu** - show/hide, pets, reset position, quit.<br> |
|
|
147
|
+
| **Folder collapsed** - bubbles tucked away beside the pet.<br> | **Tray menu** - show/hide, sessions, pets, reset position, quit.<br> |
|
|
148
148
|
|
|
149
149
|
## Supported Clients
|
|
150
150
|
|
|
@@ -217,7 +217,7 @@ non-observe mode keeps terminal input native.
|
|
|
217
217
|
| Drag | Move the pet; position is saved. |
|
|
218
218
|
| Drag corner grip | Resize from 0.5x to 2x; size is saved. |
|
|
219
219
|
| Double-click grip | Reset to normal size. |
|
|
220
|
-
| Tray icon | Open menu for
|
|
220
|
+
| Tray icon | Open menu for sessions, pets, reset, updates, and quit. |
|
|
221
221
|
|
|
222
222
|
## Commands
|
|
223
223
|
|
|
@@ -2,13 +2,6 @@
|
|
|
2
2
|
// converts these descriptors into a native Menu; keeping it pure makes the
|
|
3
3
|
// recovery controls testable.
|
|
4
4
|
|
|
5
|
-
const DISPLAY_MODES = Object.freeze([
|
|
6
|
-
{ value: "global", label: "Global" },
|
|
7
|
-
{ value: "cluster", label: "Cluster" },
|
|
8
|
-
{ value: "per-terminal", label: "Per Terminal" },
|
|
9
|
-
{ value: "hybrid", label: "Hybrid" }
|
|
10
|
-
]);
|
|
11
|
-
|
|
12
5
|
export function buildTrayTooltip() {
|
|
13
6
|
return "HAYA Pet";
|
|
14
7
|
}
|
|
@@ -22,17 +15,6 @@ export function buildTrayMenu(state = {}) {
|
|
|
22
15
|
id: "toggle_pet",
|
|
23
16
|
label: state.petVisible ? "Hide Pet" : "Show Pet"
|
|
24
17
|
},
|
|
25
|
-
{
|
|
26
|
-
id: "display_mode",
|
|
27
|
-
label: "Display Mode",
|
|
28
|
-
submenu: DISPLAY_MODES.map((mode) => ({
|
|
29
|
-
id: `display_mode:${mode.value}`,
|
|
30
|
-
label: mode.label,
|
|
31
|
-
value: mode.value,
|
|
32
|
-
type: "radio",
|
|
33
|
-
checked: state.displayMode === mode.value
|
|
34
|
-
}))
|
|
35
|
-
},
|
|
36
18
|
{
|
|
37
19
|
id: "sessions",
|
|
38
20
|
label: "Active Sessions",
|
|
@@ -43,17 +25,9 @@ export function buildTrayMenu(state = {}) {
|
|
|
43
25
|
label: "Installed Pets",
|
|
44
26
|
submenu: buildPetItems(pets, state.selectedPetId)
|
|
45
27
|
},
|
|
46
|
-
{
|
|
47
|
-
id: "attach_bubbles",
|
|
48
|
-
label: "Attach Bubbles to Terminals",
|
|
49
|
-
type: "checkbox",
|
|
50
|
-
checked: Boolean(state.attachBubblesToTerminals)
|
|
51
|
-
},
|
|
52
28
|
{ id: "reset_position", label: "Reset Position" },
|
|
53
|
-
// Parked until a real settings window exists
|
|
54
|
-
//
|
|
55
|
-
// item would be a dead button. Re-enable once settings outgrow the tray
|
|
56
|
-
// (e.g. bubble text size, linger duration) and a handler is wired up.
|
|
29
|
+
// Parked until a real settings window exists; partially implemented knobs
|
|
30
|
+
// stay hidden instead of showing dead or state-only controls.
|
|
57
31
|
// { id: "settings", label: "Open Settings" },
|
|
58
32
|
// Only present when the daily npm update check found a newer version;
|
|
59
33
|
// clicking it opens the package page (the app never runs npm itself).
|
|
@@ -13,9 +13,11 @@ const baseState = {
|
|
|
13
13
|
test("includes the documented recovery controls", () => {
|
|
14
14
|
const menu = buildTrayMenu(baseState);
|
|
15
15
|
const ids = menu.map((item) => item.id);
|
|
16
|
-
for (const id of ["toggle_pet", "
|
|
16
|
+
for (const id of ["toggle_pet", "sessions", "pets", "reset_position", "quit"]) {
|
|
17
17
|
assert.ok(ids.includes(id), `missing ${id}`);
|
|
18
18
|
}
|
|
19
|
+
assert.ok(!ids.includes("display_mode"), "display mode should stay hidden until implemented");
|
|
20
|
+
assert.ok(!ids.includes("attach_bubbles"), "attach bubbles should stay hidden until implemented");
|
|
19
21
|
// "Open Settings" is parked until a settings window exists (every current
|
|
20
22
|
// setting already has a tray/CLI/gesture home) — it must not be shown dead.
|
|
21
23
|
assert.ok(!ids.includes("settings"), "settings item should stay hidden until implemented");
|
|
@@ -26,14 +28,6 @@ test("toggles the pet label based on visibility", () => {
|
|
|
26
28
|
assert.equal(buildTrayMenu({ ...baseState, petVisible: false }).find((i) => i.id === "toggle_pet").label, "Show Pet");
|
|
27
29
|
});
|
|
28
30
|
|
|
29
|
-
test("checks the current display mode in the submenu", () => {
|
|
30
|
-
const submenu = buildTrayMenu(baseState).find((i) => i.id === "display_mode").submenu;
|
|
31
|
-
const hybrid = submenu.find((i) => i.value === "hybrid");
|
|
32
|
-
const global = submenu.find((i) => i.value === "global");
|
|
33
|
-
assert.equal(hybrid.checked, true);
|
|
34
|
-
assert.equal(global.checked, false);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
31
|
test("lists active sessions or shows an empty placeholder", () => {
|
|
38
32
|
const withSessions = buildTrayMenu(baseState).find((i) => i.id === "sessions").submenu;
|
|
39
33
|
assert.equal(withSessions[0].label, "Codex · netdisk-server");
|
|
@@ -42,11 +36,6 @@ test("lists active sessions or shows an empty placeholder", () => {
|
|
|
42
36
|
assert.equal(empty[0].enabled, false);
|
|
43
37
|
});
|
|
44
38
|
|
|
45
|
-
test("reflects the attach-bubbles checkbox state", () => {
|
|
46
|
-
assert.equal(buildTrayMenu(baseState).find((i) => i.id === "attach_bubbles").checked, true);
|
|
47
|
-
assert.equal(buildTrayMenu({ ...baseState, attachBubblesToTerminals: false }).find((i) => i.id === "attach_bubbles").checked, false);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
39
|
test("shows the update item only when a newer version is known", () => {
|
|
51
40
|
const withoutUpdate = buildTrayMenu(baseState);
|
|
52
41
|
assert.ok(!withoutUpdate.some((i) => i.id === "update"), "no update item by default");
|
package/docs/known-issues.md
CHANGED
|
@@ -117,6 +117,20 @@ Issues found in live use, with their current status.
|
|
|
117
117
|
surfaces as turn-end *idle*). The TUI's passive `/approve` denial-override
|
|
118
118
|
picker is not a blocking prompt.
|
|
119
119
|
|
|
120
|
+
## ✅ Resolved: Codex asked to review HAYA hooks on every launch
|
|
121
|
+
|
|
122
|
+
- **Symptom:** Even after approving HAYA Pet's Codex hooks once, every new
|
|
123
|
+
`haya-pet run --client codex` showed Codex's hook review prompt again.
|
|
124
|
+
- **Root cause:** HAYA Pet correctly wrote a stable
|
|
125
|
+
`$CODEX_HOME/haya-pet.config.toml` profile, but Codex stores the user's hook
|
|
126
|
+
trust decisions back into that same profile under `[hooks.state]` as
|
|
127
|
+
`trusted_hash` entries. The injector rewrote the entire profile on every
|
|
128
|
+
launch, so it deleted Codex's trust cache before Codex could reuse it.
|
|
129
|
+
- **Fix:** The Codex hook injector now regenerates the HAYA-managed hook tables
|
|
130
|
+
while preserving the Codex-managed `[hooks.state]` tables from the existing
|
|
131
|
+
profile. Users may need to approve once after updating; after that, unchanged
|
|
132
|
+
hook commands should stay trusted.
|
|
133
|
+
|
|
120
134
|
## ✅ Resolved: Codex pet looked busy immediately after startup
|
|
121
135
|
|
|
122
136
|
- **Symptom:** Starting a wrapped Codex session and doing nothing could still make
|
|
@@ -4,4 +4,4 @@ Place screenshot PNGs referenced by the root README here (~800px wide):
|
|
|
4
4
|
- `pet-overlay.png` — the pet reacting to the highest-priority session
|
|
5
5
|
- `session-bubbles.png` — bubbles expanded, showing per-session status icons
|
|
6
6
|
- `folder-collapsed.png` — bubbles folded away beside the pet
|
|
7
|
-
- `tray-menu.png` — the tray menu (show/hide, pets, reset position, Quit)
|
|
7
|
+
- `tray-menu.png` — the tray menu (show/hide, sessions, pets, reset position, Quit)
|
package/docs/troubleshooting.md
CHANGED
|
@@ -18,6 +18,7 @@ deferred problems with known root causes.
|
|
|
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
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. |
|
|
20
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. |
|
|
21
|
+
| **Codex** asks to review HAYA hooks on every launch | Fixed — update to the latest version, then approve once more. Codex writes trusted hook hashes into `$CODEX_HOME/haya-pet.config.toml` under `[hooks.state]`; HAYA Pet now preserves that Codex-managed block when refreshing the hook profile. |
|
|
21
22
|
| 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 reports **reviewing** from the permission hook itself, then **working** on an allow verdict or **thinking** on a deny. *Waiting for approval* still shows when Codex actually asks you (`approvals_reviewer = "user"`). Restart the wrapped Codex session after updating so Codex reloads the changed hook command. |
|
|
22
23
|
| Pet shows **shell_command** or **thinking** right after starting Codex, before you prompt it | Fixed — the Codex transcript and guardian watchers now ignore rollouts whose `session_meta.timestamp` predates the current wrapper launch, so another active Codex session cannot drive this pet's status. Restart the wrapped Codex session after updating. |
|
|
23
24
|
| **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
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// across sessions so Codex's hook-trust review only needs approving once. fnm hands
|
|
9
9
|
// out a per-shell symlink for process.execPath that dies when the launching shell
|
|
10
10
|
// exits, so we realpath it before baking it into the hook command.
|
|
11
|
-
import { mkdirSync, realpathSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
12
12
|
import { homedir } from "node:os";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
@@ -32,7 +32,8 @@ export function injectCodexHooks({ nodePath, cliPath, codexHome, env = process.e
|
|
|
32
32
|
// rewrite identical bytes, and the hooks stay "trusted" across launches.
|
|
33
33
|
mkdirSync(home, { recursive: true });
|
|
34
34
|
const profilePath = join(home, PROFILE_FILE);
|
|
35
|
-
|
|
35
|
+
const trustedState = readCodexHookTrustState(profilePath);
|
|
36
|
+
writeFileSync(profilePath, appendCodexHookTrustState(toml, trustedState), "utf8");
|
|
36
37
|
|
|
37
38
|
// The profile file is stable and reusable on purpose — leaving it in place is
|
|
38
39
|
// what lets Codex remember the hooks are trusted. cleanup is a no-op kept for
|
|
@@ -47,3 +48,51 @@ function safeRealpath(target) {
|
|
|
47
48
|
return target;
|
|
48
49
|
}
|
|
49
50
|
}
|
|
51
|
+
|
|
52
|
+
function readCodexHookTrustState(profilePath) {
|
|
53
|
+
try {
|
|
54
|
+
return extractCodexHookTrustState(readFileSync(profilePath, "utf8"));
|
|
55
|
+
} catch {
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function appendCodexHookTrustState(toml, trustedState) {
|
|
61
|
+
if (!trustedState) {
|
|
62
|
+
return toml;
|
|
63
|
+
}
|
|
64
|
+
return `${toml.trimEnd()}\n\n${trustedState.trim()}\n`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function extractCodexHookTrustState(toml) {
|
|
68
|
+
const lines = String(toml).split(/\r?\n/);
|
|
69
|
+
const output = [];
|
|
70
|
+
let inHookState = false;
|
|
71
|
+
|
|
72
|
+
for (const line of lines) {
|
|
73
|
+
const tableName = readTomlTableName(line);
|
|
74
|
+
if (tableName) {
|
|
75
|
+
const isHookStateTable = tableName === "hooks.state" || tableName.startsWith("hooks.state.");
|
|
76
|
+
if (isHookStateTable) {
|
|
77
|
+
inHookState = true;
|
|
78
|
+
} else if (inHookState) {
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (inHookState) {
|
|
84
|
+
output.push(line);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return output.join("\n").trim();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function readTomlTableName(line) {
|
|
92
|
+
const table = /^\s*\[([^\]]+)\]\s*$/.exec(line);
|
|
93
|
+
if (table) {
|
|
94
|
+
return table[1];
|
|
95
|
+
}
|
|
96
|
+
const arrayTable = /^\s*\[\[([^\]]+)\]\]\s*$/.exec(line);
|
|
97
|
+
return arrayTable?.[1];
|
|
98
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { test } from "../../../test/harness.mjs";
|
|
@@ -43,3 +43,32 @@ test("injectCodexHooks honors CODEX_HOME from env and is stable across calls", (
|
|
|
43
43
|
rmSync(home, { recursive: true, force: true });
|
|
44
44
|
}
|
|
45
45
|
});
|
|
46
|
+
|
|
47
|
+
test("injectCodexHooks preserves Codex hook trust state in the managed profile", () => {
|
|
48
|
+
const home = mkdtempSync(join(tmpdir(), "haya-codex-home-"));
|
|
49
|
+
try {
|
|
50
|
+
const first = injectCodexHooks({
|
|
51
|
+
nodePath: "n",
|
|
52
|
+
cliPath: "c",
|
|
53
|
+
codexHome: home
|
|
54
|
+
});
|
|
55
|
+
const trustedState = `[hooks.state]
|
|
56
|
+
|
|
57
|
+
[hooks.state.'${first.profilePath}:user_prompt_submit:0:0']
|
|
58
|
+
trusted_hash = "sha256:abc123"
|
|
59
|
+
`;
|
|
60
|
+
writeFileSync(first.profilePath, `${readFileSync(first.profilePath, "utf8")}\n${trustedState}`, "utf8");
|
|
61
|
+
|
|
62
|
+
injectCodexHooks({
|
|
63
|
+
nodePath: "n",
|
|
64
|
+
cliPath: "c",
|
|
65
|
+
codexHome: home
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const next = readFileSync(first.profilePath, "utf8");
|
|
69
|
+
assert.match(next, /\[hooks\.state\]/);
|
|
70
|
+
assert.match(next, /trusted_hash = "sha256:abc123"/);
|
|
71
|
+
} finally {
|
|
72
|
+
rmSync(home, { recursive: true, force: true });
|
|
73
|
+
}
|
|
74
|
+
});
|