@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 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
- title.innerHTML = `<span class="client">${escapeHtml(bubble.clientName)}</span> ` +
243
- `<span class="project">${escapeHtml(bubble.projectName)}</span>`;
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, "&amp;")
264
- .replace(/</g, "&lt;")
265
- .replace(/>/g, "&gt;");
266
- }
@@ -209,6 +209,7 @@ body {
209
209
 
210
210
  .bubble .client {
211
211
  font-weight: 600;
212
+ margin-right: 0.4em;
212
213
  }
213
214
 
214
215
  .bubble .project {
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@hayasaka7/haya-pet",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "type": "module",
5
5
  "description": "Generic AI CLI pet runtime foundation.",
6
6
  "keywords": [
@@ -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
+ });