@hayasaka7/haya-pet 0.3.1 → 0.3.2
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 +10 -0
- 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/package.json +1 -1
- 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,16 @@ 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.2]
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- **Session bubble titles no longer run off the screen.** A long project name
|
|
14
|
+
used to stretch the bubble out to the panel's full width. The title now keeps
|
|
15
|
+
the **client name in full** (Codex, Claude Code, …) and shows the **project
|
|
16
|
+
name capped at 10 characters** with an ellipsis when it's longer (e.g.
|
|
17
|
+
`netdisk-server` → `netdisk-se...`); the complete `Client · Project` is kept
|
|
18
|
+
as a hover tooltip so nothing is lost.
|
|
19
|
+
|
|
10
20
|
## [0.3.1]
|
|
11
21
|
|
|
12
22
|
### Fixed
|
|
@@ -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/package.json
CHANGED
|
@@ -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
|
+
});
|