@hayasaka7/haya-pet 0.2.1 → 0.2.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.
@@ -0,0 +1,75 @@
1
+ name: CI
2
+
3
+ # Run code quality checks and the test suite on every push that touches code.
4
+ on:
5
+ push:
6
+ paths:
7
+ - "**/*.js"
8
+ - "**/*.mjs"
9
+ - "**/*.cjs"
10
+ - "package.json"
11
+ - "package-lock.json"
12
+ - ".github/workflows/ci.yml"
13
+ pull_request:
14
+ paths:
15
+ - "**/*.js"
16
+ - "**/*.mjs"
17
+ - "**/*.cjs"
18
+ - "package.json"
19
+ - "package-lock.json"
20
+ - ".github/workflows/ci.yml"
21
+
22
+ concurrency:
23
+ group: ci-${{ github.workflow }}-${{ github.ref }}
24
+ cancel-in-progress: true
25
+
26
+ permissions:
27
+ contents: read
28
+
29
+ jobs:
30
+ lint:
31
+ name: Code quality (ESLint)
32
+ runs-on: ubuntu-latest
33
+ steps:
34
+ - uses: actions/checkout@v4
35
+
36
+ - name: Set up Node.js
37
+ uses: actions/setup-node@v4
38
+ with:
39
+ node-version: 22
40
+ cache: npm
41
+
42
+ - name: Install dependencies
43
+ # Electron's binary isn't needed for linting or tests; skip the ~150 MB
44
+ # download so CI is fast and isn't at the mercy of the Electron CDN.
45
+ env:
46
+ ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
47
+ run: npm ci
48
+
49
+ - name: Run ESLint
50
+ run: npm run lint
51
+
52
+ test:
53
+ name: Tests (Node ${{ matrix.node }} on ${{ matrix.os }})
54
+ runs-on: ${{ matrix.os }}
55
+ strategy:
56
+ fail-fast: false
57
+ matrix:
58
+ os: [ubuntu-latest, windows-latest, macos-latest]
59
+ node: [20, 22]
60
+ steps:
61
+ - uses: actions/checkout@v4
62
+
63
+ - name: Set up Node.js
64
+ uses: actions/setup-node@v4
65
+ with:
66
+ node-version: ${{ matrix.node }}
67
+ cache: npm
68
+
69
+ - name: Install dependencies
70
+ env:
71
+ ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
72
+ run: npm ci
73
+
74
+ - name: Run the test suite
75
+ run: npm test
package/CHANGELOG.md CHANGED
@@ -7,6 +7,24 @@ 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.2.2]
11
+
12
+ ### Fixed
13
+ - **Session bubbles no longer reshuffle while sessions run.** Bubbles used to be
14
+ sorted by state urgency and latest activity, so every status change could move
15
+ a bubble up or down the stack mid-progress. They now stack by the time each
16
+ session **connected to the pet** — newest on top, first one at the bottom —
17
+ and that order stays fixed for the session's whole life. Urgency still shows
18
+ through each bubble's status icon, the collapsed-folder summary dot, and the
19
+ pet animation.
20
+
21
+ ### Internal
22
+ - **CI on every code push** — a new GitHub Actions workflow lints and runs the
23
+ test suite (Ubuntu + Windows + macOS, Node 20/22) for any push or PR touching
24
+ code.
25
+ - **ESLint adopted** (`npm run lint`, flat config); the few existing findings
26
+ were fixed with no behavior change.
27
+
10
28
  ## [0.2.1]
11
29
 
12
30
  ### Added
package/README.md CHANGED
@@ -38,8 +38,9 @@ Haya Pet watches all of them and presents one ambient interface:
38
38
  draggable, and position-persistent like a real desktop companion.
39
39
  - **Session bubbles** — one compact bubble per active session showing client,
40
40
  project, the latest activity, and a status icon (a spinning *working* circle, a
41
- green *done* check, a yellow *needs you*, or a red *failed* cross). A folder
42
- button beside the pet folds them away.
41
+ green *done* check, a yellow *needs you*, or a red *failed* cross). Bubbles stack
42
+ by connect time — the newest session on top — so the stack never reshuffles while
43
+ work is in progress. A folder button beside the pet folds them away.
43
44
 
44
45
  ## Features
45
46
 
@@ -121,7 +121,7 @@ function renderComposer(bubble, replyMode, bridge) {
121
121
  return composer;
122
122
  }
123
123
 
124
- function renderControls(controlsPromise, bubble, bridge) {
124
+ function renderControls(controlsPromise, _bubble, _bridge) {
125
125
  const wrap = document.createElement("div");
126
126
  wrap.className = "controls";
127
127
 
@@ -0,0 +1,32 @@
1
+ import js from "@eslint/js";
2
+ import globals from "globals";
3
+
4
+ export default [
5
+ {
6
+ ignores: [
7
+ "node_modules/**",
8
+ "native/**",
9
+ "tmp/**",
10
+ "docs/**",
11
+ ".gax/**",
12
+ ],
13
+ },
14
+ js.configs.recommended,
15
+ {
16
+ files: ["**/*.js", "**/*.mjs", "**/*.cjs"],
17
+ languageOptions: {
18
+ ecmaVersion: "latest",
19
+ sourceType: "module",
20
+ globals: {
21
+ ...globals.node,
22
+ ...globals.browser,
23
+ },
24
+ },
25
+ rules: {
26
+ "no-unused-vars": [
27
+ "error",
28
+ { argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
29
+ ],
30
+ },
31
+ },
32
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hayasaka7/haya-pet",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "description": "Generic AI CLI pet runtime foundation.",
6
6
  "license": "MIT",
@@ -17,6 +17,7 @@
17
17
  "haya-pet": "apps/cli/src/haya-pet.js"
18
18
  },
19
19
  "scripts": {
20
+ "lint": "eslint .",
20
21
  "test": "node test/run-tests.mjs"
21
22
  },
22
23
  "workspaces": [
@@ -31,5 +32,10 @@
31
32
  },
32
33
  "engines": {
33
34
  "node": ">=16.20.0"
35
+ },
36
+ "devDependencies": {
37
+ "@eslint/js": "^10.0.1",
38
+ "eslint": "^10.4.1",
39
+ "globals": "^17.6.0"
34
40
  }
35
41
  }
@@ -256,7 +256,6 @@ async function runObservedCommand({
256
256
  registered = true;
257
257
  if (pendingState) {
258
258
  emitState(pendingState);
259
- pendingState = undefined;
260
259
  }
261
260
 
262
261
  await sendProtocolMessage(send, { type: "heartbeat", sessionId, updatedAt: now() });
@@ -1,5 +1,4 @@
1
1
  import { mapAiStateToPetAction } from "../../pet-core/src/atlas.js";
2
- import { getSessionPriorityRank } from "./priority.js";
3
2
  import { buildSessionSummary, buildStatusLabel, formatElapsed } from "./summaries.js";
4
3
 
5
4
  // Collapses the full AI-state vocabulary into the four progress kinds the
@@ -53,17 +52,21 @@ export function buildBubbleViews(sessions, now = Date.now(), options = {}) {
53
52
  return sessions
54
53
  .filter(Boolean)
55
54
  .slice()
56
- .sort(compareByPriority)
55
+ .sort(compareByConnectTime)
57
56
  .map((session) => buildBubbleView(session, now, options));
58
57
  }
59
58
 
60
- function compareByPriority(left, right) {
61
- const rankDelta = getSessionPriorityRank(left) - getSessionPriorityRank(right);
62
- if (rankDelta !== 0) {
63
- return rankDelta;
59
+ // Bubbles stack by connect time — the newest session on top, the first one at
60
+ // the bottom and never reshuffle while sessions are in progress. State
61
+ // urgency only drives the collapsed-folder dot and the pet animation, not the
62
+ // list order.
63
+ function compareByConnectTime(left, right) {
64
+ const startedDelta = numeric(right.startedAt) - numeric(left.startedAt);
65
+ if (startedDelta !== 0) {
66
+ return startedDelta;
64
67
  }
65
68
 
66
- return numeric(right.updatedAt) - numeric(left.updatedAt);
69
+ return String(left.sessionId).localeCompare(String(right.sessionId));
67
70
  }
68
71
 
69
72
  function safePetAction(state) {
@@ -28,15 +28,40 @@ test("builds a bubble view model with label, summary, action, and elapsed", () =
28
28
  assert.equal(view.elapsedLabel, "1m 4s");
29
29
  });
30
30
 
31
- test("orders bubbles by session priority then recency", () => {
31
+ test("stacks bubbles by connect time, newest on top, so they never reshuffle mid-session", () => {
32
32
  const sessions = [
33
- { ...baseSession, sessionId: "sess_idle", state: "idle", updatedAt: 9_000 },
34
- { ...baseSession, sessionId: "sess_wait", state: "waiting_approval", updatedAt: 4_000 },
35
- { ...baseSession, sessionId: "sess_run", state: "running_tool", updatedAt: 8_000 }
33
+ { ...baseSession, sessionId: "sess_third", state: "waiting_approval", startedAt: 3_000, updatedAt: 4_000 },
34
+ { ...baseSession, sessionId: "sess_first", state: "idle", startedAt: 1_000, updatedAt: 9_000 },
35
+ { ...baseSession, sessionId: "sess_second", state: "running_tool", startedAt: 2_000, updatedAt: 8_000 }
36
36
  ];
37
37
 
38
38
  const views = buildBubbleViews(sessions, 10_000);
39
- assert.deepEqual(views.map((view) => view.sessionId), ["sess_wait", "sess_run", "sess_idle"]);
39
+ assert.deepEqual(views.map((view) => view.sessionId), ["sess_third", "sess_second", "sess_first"]);
40
+ });
41
+
42
+ test("keeps bubble order stable when states and activity change", () => {
43
+ const before = [
44
+ { ...baseSession, sessionId: "sess_first", state: "running_tool", startedAt: 1_000, updatedAt: 2_000 },
45
+ { ...baseSession, sessionId: "sess_second", state: "idle", startedAt: 2_000, updatedAt: 2_500 }
46
+ ];
47
+ // Later, the second session becomes urgent and more recently active.
48
+ const after = [
49
+ { ...baseSession, sessionId: "sess_first", state: "idle", startedAt: 1_000, updatedAt: 3_000 },
50
+ { ...baseSession, sessionId: "sess_second", state: "waiting_approval", startedAt: 2_000, updatedAt: 9_000 }
51
+ ];
52
+
53
+ const order = (sessions) => buildBubbleViews(sessions, 10_000).map((view) => view.sessionId);
54
+ assert.deepEqual(order(before), order(after));
55
+ });
56
+
57
+ test("breaks connect-time ties by session id for a deterministic order", () => {
58
+ const sessions = [
59
+ { ...baseSession, sessionId: "sess_b", startedAt: 1_000 },
60
+ { ...baseSession, sessionId: "sess_a", startedAt: 1_000 }
61
+ ];
62
+
63
+ const views = buildBubbleViews(sessions, 10_000);
64
+ assert.deepEqual(views.map((view) => view.sessionId), ["sess_a", "sess_b"]);
40
65
  });
41
66
 
42
67
  test("marks the selected/pinned session", () => {