heyio 0.30.0 → 0.31.0
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/dist/api/server.js +5 -5
- package/dist/copilot/instance-deactivate.test.js +119 -0
- package/dist/copilot/scheduler.js +1 -1
- package/dist/copilot/tools.js +51 -2
- package/dist/store/db.js +21 -1
- package/dist/store/feed.js +3 -3
- package/dist/store/feed.test.js +20 -20
- package/dist/store/squads.js +1 -1
- package/dist/store/squads.test.js +353 -0
- package/dist/tui/index.js +2 -2
- package/package.json +2 -2
- package/web-dist/assets/{index-D3uXBVcQ.js → index-4dkSQDXb.js} +27 -27
- package/web-dist/assets/index-DK5ySkTW.css +10 -0
- package/web-dist/index.html +2 -2
- package/web-dist/assets/index-DmthMbtN.css +0 -10
package/dist/api/server.js
CHANGED
|
@@ -105,7 +105,7 @@ export async function startApiServer() {
|
|
|
105
105
|
api.get("/feed/count", (req, res) => {
|
|
106
106
|
try {
|
|
107
107
|
const rawType = req.query.type;
|
|
108
|
-
const type = rawType === "
|
|
108
|
+
const type = rawType === "inbox" || rawType === "notification"
|
|
109
109
|
? rawType
|
|
110
110
|
: undefined;
|
|
111
111
|
const count = countUnreadFeedEntries(type);
|
|
@@ -119,7 +119,7 @@ export async function startApiServer() {
|
|
|
119
119
|
api.get("/feed", (req, res) => {
|
|
120
120
|
try {
|
|
121
121
|
const rawType = req.query.type;
|
|
122
|
-
const type = rawType === "
|
|
122
|
+
const type = rawType === "inbox" || rawType === "notification"
|
|
123
123
|
? rawType
|
|
124
124
|
: undefined;
|
|
125
125
|
const unreadOnly = req.query.unread === "true";
|
|
@@ -253,7 +253,7 @@ export async function startApiServer() {
|
|
|
253
253
|
api.post("/feed/read-all", (req, res) => {
|
|
254
254
|
try {
|
|
255
255
|
const rawType = req.query.type;
|
|
256
|
-
const type = rawType === "
|
|
256
|
+
const type = rawType === "inbox" || rawType === "notification"
|
|
257
257
|
? rawType
|
|
258
258
|
: undefined;
|
|
259
259
|
const marked = markAllFeedEntriesRead(type);
|
|
@@ -316,8 +316,8 @@ export async function startApiServer() {
|
|
|
316
316
|
});
|
|
317
317
|
api.post("/feed", (req, res) => {
|
|
318
318
|
const { type, title, body, source_type, source_ref } = req.body;
|
|
319
|
-
if (type !== "
|
|
320
|
-
res.status(400).json({ error: "type must be '
|
|
319
|
+
if (type !== "inbox" && type !== "notification") {
|
|
320
|
+
res.status(400).json({ error: "type must be 'inbox' or 'notification'" });
|
|
321
321
|
return;
|
|
322
322
|
}
|
|
323
323
|
if (!title || typeof title !== "string" || title.trim() === "") {
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for auto-deactivation of activeInstanceId on instance complete/abort.
|
|
3
|
+
* Exercises the squad_instance_complete and squad_instance_abort tool handlers.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, beforeEach } from "node:test";
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { createTools } from "./tools.js";
|
|
8
|
+
// Minimal mock deps sufficient to test the instance tools
|
|
9
|
+
function makeMockDeps(overrides = {}) {
|
|
10
|
+
const instances = {};
|
|
11
|
+
const base = {
|
|
12
|
+
wikiRead: () => undefined,
|
|
13
|
+
wikiWrite: () => { },
|
|
14
|
+
wikiSearch: () => [],
|
|
15
|
+
wikiAssertPagePath: () => { },
|
|
16
|
+
wikiDelete: () => false,
|
|
17
|
+
wikiList: () => [],
|
|
18
|
+
getSquad: () => ({ slug: "test", name: "Test", projectPath: "/tmp/test", status: "idle" }),
|
|
19
|
+
listSquads: () => [],
|
|
20
|
+
createSquad: () => { },
|
|
21
|
+
deleteSquad: () => { },
|
|
22
|
+
logDecision: () => { },
|
|
23
|
+
getDecisionsSummary: () => "",
|
|
24
|
+
getRecentDecisions: () => [],
|
|
25
|
+
updateSquadStatus: () => { },
|
|
26
|
+
delegateToAgent: async () => "task-1",
|
|
27
|
+
getTask: () => undefined,
|
|
28
|
+
getActiveAgentTasks: () => [],
|
|
29
|
+
addSquadAgent: () => ({ character_name: "A", role_title: "R", personality: null, model_tier: "medium" }),
|
|
30
|
+
listSquadAgents: () => [],
|
|
31
|
+
getAgentTaskStats: () => [],
|
|
32
|
+
getStalestSpecialist: () => null,
|
|
33
|
+
removeSquadAgent: () => false,
|
|
34
|
+
resetSquadAgent: () => ({ found: false, previousStatus: "", agent: null }),
|
|
35
|
+
setSquadLead: () => { },
|
|
36
|
+
getSquadLead: () => undefined,
|
|
37
|
+
setSquadQA: () => { },
|
|
38
|
+
getTaskReviews: () => [],
|
|
39
|
+
getSquadWorkDistribution: () => ({ total: 0, perAgent: [] }),
|
|
40
|
+
listSkills: () => [],
|
|
41
|
+
installSkill: async () => ({ name: "", slug: "", description: "", path: "" }),
|
|
42
|
+
removeSkill: () => false,
|
|
43
|
+
searchSkillsRegistry: async () => [],
|
|
44
|
+
saveConfig: () => { },
|
|
45
|
+
checkForUpdate: async () => ({ updateAvailable: false, current: "1.0.0", latest: "1.0.0" }),
|
|
46
|
+
// Instance deps
|
|
47
|
+
createInstance: (input) => {
|
|
48
|
+
const inst = { id: input.id, master_squad_slug: input.masterSquadSlug, status: "pending", worktree_path: input.worktreePath, branch_name: input.branchName, issue_ref: null, context_snapshot: null, created_at: new Date().toISOString(), completed_at: null };
|
|
49
|
+
instances[input.id] = inst;
|
|
50
|
+
return inst;
|
|
51
|
+
},
|
|
52
|
+
getInstance: (id) => instances[id],
|
|
53
|
+
listInstances: () => [],
|
|
54
|
+
updateInstanceStatus: (id, status) => { if (instances[id])
|
|
55
|
+
instances[id].status = status; },
|
|
56
|
+
logInstanceDecision: () => { },
|
|
57
|
+
getInstanceDecisions: () => [],
|
|
58
|
+
mergeInstanceDecisions: () => 0,
|
|
59
|
+
deleteInstance: (id) => { delete instances[id]; },
|
|
60
|
+
buildContextSnapshot: () => "[]",
|
|
61
|
+
reconcileInstances: () => 0,
|
|
62
|
+
createWorktree: () => "/tmp/wt",
|
|
63
|
+
removeWorktree: () => { },
|
|
64
|
+
activeInstanceId: undefined,
|
|
65
|
+
...overrides,
|
|
66
|
+
};
|
|
67
|
+
// Pre-seed an instance for tests
|
|
68
|
+
instances["test-squad--issue-1"] = {
|
|
69
|
+
id: "test-squad--issue-1",
|
|
70
|
+
master_squad_slug: "test",
|
|
71
|
+
issue_ref: "#1",
|
|
72
|
+
worktree_path: "/tmp/wt/test-squad--issue-1",
|
|
73
|
+
branch_name: "test/instance/issue-1",
|
|
74
|
+
status: "active",
|
|
75
|
+
context_snapshot: null,
|
|
76
|
+
created_at: new Date().toISOString(),
|
|
77
|
+
completed_at: null,
|
|
78
|
+
};
|
|
79
|
+
return base;
|
|
80
|
+
}
|
|
81
|
+
function findToolHandler(tools, name) {
|
|
82
|
+
const tool = tools.find((t) => t.name === name);
|
|
83
|
+
if (!tool)
|
|
84
|
+
throw new Error(`Tool not found: ${name}`);
|
|
85
|
+
return tool.handler;
|
|
86
|
+
}
|
|
87
|
+
describe("auto-deactivate activeInstanceId", () => {
|
|
88
|
+
let deps;
|
|
89
|
+
let tools;
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
deps = makeMockDeps();
|
|
92
|
+
tools = createTools(deps);
|
|
93
|
+
});
|
|
94
|
+
it("completing an active instance auto-deactivates it", async () => {
|
|
95
|
+
deps.activeInstanceId = "test-squad--issue-1";
|
|
96
|
+
const handler = findToolHandler(tools, "squad_instance_complete");
|
|
97
|
+
await handler({ instance_id: "test-squad--issue-1" });
|
|
98
|
+
assert.strictEqual(deps.activeInstanceId, undefined);
|
|
99
|
+
});
|
|
100
|
+
it("aborting an active instance auto-deactivates it", async () => {
|
|
101
|
+
deps.activeInstanceId = "test-squad--issue-1";
|
|
102
|
+
const handler = findToolHandler(tools, "squad_instance_abort");
|
|
103
|
+
await handler({ instance_id: "test-squad--issue-1" });
|
|
104
|
+
assert.strictEqual(deps.activeInstanceId, undefined);
|
|
105
|
+
});
|
|
106
|
+
it("completing a non-active instance does NOT change activeInstanceId", async () => {
|
|
107
|
+
deps.activeInstanceId = "some-other-instance";
|
|
108
|
+
const handler = findToolHandler(tools, "squad_instance_complete");
|
|
109
|
+
await handler({ instance_id: "test-squad--issue-1" });
|
|
110
|
+
assert.strictEqual(deps.activeInstanceId, "some-other-instance");
|
|
111
|
+
});
|
|
112
|
+
it("aborting a non-active instance does NOT change activeInstanceId", async () => {
|
|
113
|
+
deps.activeInstanceId = "some-other-instance";
|
|
114
|
+
const handler = findToolHandler(tools, "squad_instance_abort");
|
|
115
|
+
await handler({ instance_id: "test-squad--issue-1" });
|
|
116
|
+
assert.strictEqual(deps.activeInstanceId, "some-other-instance");
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
//# sourceMappingURL=instance-deactivate.test.js.map
|
|
@@ -83,7 +83,7 @@ async function fireSchedule(schedule) {
|
|
|
83
83
|
try {
|
|
84
84
|
await delegateToAgent(squad.slug, prompt, (_taskId, result) => {
|
|
85
85
|
if (shouldRouteToInbox(prompt)) {
|
|
86
|
-
createFeedEntry({ type: "
|
|
86
|
+
createFeedEntry({ type: "inbox", title: `[${squad.slug}] ${schedule.name}`, body: result });
|
|
87
87
|
console.error(`[io] Schedule ${schedule.id} result routed to inbox`);
|
|
88
88
|
completeScheduleRun(run.id, 0);
|
|
89
89
|
}
|
package/dist/copilot/tools.js
CHANGED
|
@@ -407,7 +407,7 @@ export function createTools(deps) {
|
|
|
407
407
|
const taskId = await deps.delegateToAgent(slug, task, (id, result) => {
|
|
408
408
|
console.error(`[io] Agent task ${id} completed for squad ${slug}`);
|
|
409
409
|
if (shouldRouteToInbox(task)) {
|
|
410
|
-
createFeedEntry({ type: "
|
|
410
|
+
createFeedEntry({ type: "inbox", title: `[${slug}] Task result`, body: result, squad_slug: slug });
|
|
411
411
|
console.error(`[io] Task ${id} result routed to inbox`);
|
|
412
412
|
}
|
|
413
413
|
}, agent, deps.activeInstanceId);
|
|
@@ -1811,6 +1811,10 @@ export function createTools(deps) {
|
|
|
1811
1811
|
const projectPath = squad?.projectPath ?? instance.worktree_path.replace(/\/\.io-worktrees\/.*$/, "");
|
|
1812
1812
|
deps.removeWorktree(projectPath, instance.worktree_path);
|
|
1813
1813
|
deps.updateInstanceStatus(instance_id, "done");
|
|
1814
|
+
// Auto-deactivate if this was the active instance
|
|
1815
|
+
if (deps.activeInstanceId === instance_id) {
|
|
1816
|
+
deps.activeInstanceId = undefined;
|
|
1817
|
+
}
|
|
1814
1818
|
return `Instance "${instance_id}" completed.\n- ${merged} decision(s) merged to master squad "${instance.master_squad_slug}"\n- Worktree cleaned up`;
|
|
1815
1819
|
}
|
|
1816
1820
|
catch (err) {
|
|
@@ -1832,6 +1836,10 @@ export function createTools(deps) {
|
|
|
1832
1836
|
return `Instance already in terminal state: ${instance.status}`;
|
|
1833
1837
|
}
|
|
1834
1838
|
deps.updateInstanceStatus(instance_id, "failed");
|
|
1839
|
+
// Auto-deactivate if this was the active instance
|
|
1840
|
+
if (deps.activeInstanceId === instance_id) {
|
|
1841
|
+
deps.activeInstanceId = undefined;
|
|
1842
|
+
}
|
|
1835
1843
|
return `Instance "${instance_id}" aborted. Worktree preserved at: ${instance.worktree_path}\nUse squad_instance_cleanup to remove it.`;
|
|
1836
1844
|
},
|
|
1837
1845
|
});
|
|
@@ -1885,7 +1893,48 @@ export function createTools(deps) {
|
|
|
1885
1893
|
return prev ? `Instance context deactivated (was: ${prev})` : `No instance context was active.`;
|
|
1886
1894
|
},
|
|
1887
1895
|
});
|
|
1888
|
-
|
|
1896
|
+
const sendToInbox = defineTool("send_to_inbox", {
|
|
1897
|
+
description: "Send a message directly to Michael's IO inbox. Use this to deliver results, reports, summaries, or any content that should appear in the inbox feed.",
|
|
1898
|
+
skipPermission: true,
|
|
1899
|
+
parameters: z.object({
|
|
1900
|
+
title: z.string().describe("Short title for the inbox item"),
|
|
1901
|
+
body: z.string().describe("Full content/body of the message (supports markdown)"),
|
|
1902
|
+
squad_slug: z.string().optional().describe("Squad slug to prefix the title with (e.g. 'io-assistant')"),
|
|
1903
|
+
instance_id: z.string().optional().describe("Instance ID if this message is from a squad instance"),
|
|
1904
|
+
task_id: z.string().optional().describe("Task ID associated with this message"),
|
|
1905
|
+
}),
|
|
1906
|
+
handler: async ({ title, body, squad_slug, instance_id, task_id }) => {
|
|
1907
|
+
const prefix = squad_slug ? `[${squad_slug}] ` : "";
|
|
1908
|
+
createFeedEntry({
|
|
1909
|
+
type: "inbox",
|
|
1910
|
+
title: `${prefix}${title}`,
|
|
1911
|
+
body,
|
|
1912
|
+
squad_slug: squad_slug ?? null,
|
|
1913
|
+
instance_id: instance_id ?? null,
|
|
1914
|
+
task_id: task_id ?? null,
|
|
1915
|
+
});
|
|
1916
|
+
return "Message sent to inbox successfully.";
|
|
1917
|
+
},
|
|
1918
|
+
});
|
|
1919
|
+
const sendNotification = defineTool("send_notification", {
|
|
1920
|
+
description: "Send a short status notification to the IO feed. Use for brief updates, alerts, and FYIs (one sentence). For longer content, use send_to_inbox instead.",
|
|
1921
|
+
skipPermission: true,
|
|
1922
|
+
parameters: z.object({
|
|
1923
|
+
message: z.string().describe("Short notification message (one sentence)"),
|
|
1924
|
+
squad_slug: z.string().optional().describe("Squad slug for context"),
|
|
1925
|
+
}),
|
|
1926
|
+
handler: async ({ message, squad_slug }) => {
|
|
1927
|
+
const prefix = squad_slug ? `[${squad_slug}] ` : "";
|
|
1928
|
+
createFeedEntry({
|
|
1929
|
+
type: "notification",
|
|
1930
|
+
title: `${prefix}${message}`,
|
|
1931
|
+
body: message,
|
|
1932
|
+
squad_slug: squad_slug ?? null,
|
|
1933
|
+
});
|
|
1934
|
+
return "Notification sent.";
|
|
1935
|
+
},
|
|
1936
|
+
});
|
|
1937
|
+
return [wikiRead, wikiWrite, wikiSearch, wikiDelete, wikiList, squadCreate, squadRecall, squadStatus, squadLogDecision, squadDelegate, squadTaskStatus, squadDelete, squadAnalyze, squadAddAgent, squadAgents, squadRemoveAgent, squadResetAgent, squadSetLead, squadSetQA, squadTaskReviews, squadScheduleCreate, squadScheduleList, squadScheduleDelete, squadSchedulePause, squadScheduleResume, squadScheduleRunNow, scheduleCreate, scheduleList, scheduleDelete, schedulePause, scheduleResume, scheduleRunNow, skillList, skillInstall, skillRemove, skillSearch, configUpdate, checkUpdate, shell, fileOps, bash, readFile, viewTool, grepTool, strReplaceEditor, github, squadInstanceCreate, squadInstanceList, squadInstanceStatus, squadInstanceComplete, squadInstanceAbort, squadInstanceCleanup, squadInstanceActivate, squadInstanceDeactivate, sendToInbox, sendNotification];
|
|
1889
1938
|
}
|
|
1890
1939
|
function walkDirectory(dir, maxDepth = 3, depth = 0) {
|
|
1891
1940
|
if (depth >= maxDepth)
|
package/dist/store/db.js
CHANGED
|
@@ -162,7 +162,7 @@ GROUP BY agent_slug`,
|
|
|
162
162
|
)`,
|
|
163
163
|
`CREATE TABLE IF NOT EXISTS unified_feed (
|
|
164
164
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
165
|
-
type TEXT NOT NULL CHECK(type IN ('
|
|
165
|
+
type TEXT NOT NULL CHECK(type IN ('inbox', 'notification')),
|
|
166
166
|
title TEXT NOT NULL,
|
|
167
167
|
body TEXT NOT NULL,
|
|
168
168
|
source_type TEXT,
|
|
@@ -193,6 +193,26 @@ GROUP BY agent_slug`,
|
|
|
193
193
|
)`,
|
|
194
194
|
`ALTER TABLE agent_tasks ADD COLUMN instance_id TEXT`,
|
|
195
195
|
`CREATE INDEX IF NOT EXISTS idx_instance_decisions_instance ON instance_decisions(instance_id, merged_to_master)`,
|
|
196
|
+
`ALTER TABLE unified_feed RENAME TO unified_feed_old`,
|
|
197
|
+
`CREATE TABLE unified_feed (
|
|
198
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
199
|
+
type TEXT NOT NULL CHECK(type IN ('inbox', 'notification')),
|
|
200
|
+
title TEXT NOT NULL,
|
|
201
|
+
body TEXT NOT NULL,
|
|
202
|
+
source_type TEXT,
|
|
203
|
+
source_ref TEXT,
|
|
204
|
+
squad_slug TEXT,
|
|
205
|
+
instance_id TEXT,
|
|
206
|
+
task_id TEXT,
|
|
207
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
208
|
+
read_at DATETIME
|
|
209
|
+
)`,
|
|
210
|
+
`INSERT INTO unified_feed (id, type, title, body, source_type, source_ref, created_at, read_at)
|
|
211
|
+
SELECT id, CASE WHEN type='deliverable' THEN 'inbox' ELSE type END, title, body, source_type, source_ref, created_at, read_at
|
|
212
|
+
FROM unified_feed_old`,
|
|
213
|
+
`DROP TABLE unified_feed_old`,
|
|
214
|
+
`CREATE INDEX IF NOT EXISTS idx_unified_feed_type ON unified_feed(type, created_at)`,
|
|
215
|
+
`CREATE INDEX IF NOT EXISTS idx_unified_feed_unread ON unified_feed(read_at, created_at)`,
|
|
196
216
|
];
|
|
197
217
|
for (const migration of migrations) {
|
|
198
218
|
try {
|
package/dist/store/feed.js
CHANGED
|
@@ -2,9 +2,9 @@ import { getDb } from "./db.js";
|
|
|
2
2
|
export function createFeedEntry(input) {
|
|
3
3
|
const db = getDb();
|
|
4
4
|
const info = db
|
|
5
|
-
.prepare(`INSERT INTO unified_feed (type, title, body, source_type, source_ref)
|
|
6
|
-
VALUES (?, ?, ?, ?, ?)`)
|
|
7
|
-
.run(input.type, input.title, input.body, input.source_type ?? null, input.source_ref ?? null);
|
|
5
|
+
.prepare(`INSERT INTO unified_feed (type, title, body, source_type, source_ref, squad_slug, instance_id, task_id)
|
|
6
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
7
|
+
.run(input.type, input.title, input.body, input.source_type ?? null, input.source_ref ?? null, input.squad_slug ?? null, input.instance_id ?? null, input.task_id ?? null);
|
|
8
8
|
return db
|
|
9
9
|
.prepare("SELECT * FROM unified_feed WHERE id = ?")
|
|
10
10
|
.get(info.lastInsertRowid);
|
package/dist/store/feed.test.js
CHANGED
|
@@ -27,8 +27,8 @@ beforeEach(() => {
|
|
|
27
27
|
// ── createFeedEntry ───────────────────────────────────────────────────────────
|
|
28
28
|
describe("createFeedEntry", () => {
|
|
29
29
|
it("creates a deliverable entry with correct fields", () => {
|
|
30
|
-
const entry = createFeedEntry({ type: "
|
|
31
|
-
assert.equal(entry.type, "
|
|
30
|
+
const entry = createFeedEntry({ type: "inbox", title: "Task done", body: "Here are the results." });
|
|
31
|
+
assert.equal(entry.type, "inbox");
|
|
32
32
|
assert.equal(entry.title, "Task done");
|
|
33
33
|
assert.equal(entry.body, "Here are the results.");
|
|
34
34
|
assert.equal(entry.read_at, null);
|
|
@@ -54,7 +54,7 @@ describe("createFeedEntry", () => {
|
|
|
54
54
|
assert.equal(entry.source_ref, JSON.stringify({ id: 42 }));
|
|
55
55
|
});
|
|
56
56
|
it("autoincrements ids", () => {
|
|
57
|
-
const a = createFeedEntry({ type: "
|
|
57
|
+
const a = createFeedEntry({ type: "inbox", title: "A", body: "a" });
|
|
58
58
|
const b = createFeedEntry({ type: "notification", title: "B", body: "b" });
|
|
59
59
|
assert.ok(b.id > a.id);
|
|
60
60
|
});
|
|
@@ -62,7 +62,7 @@ describe("createFeedEntry", () => {
|
|
|
62
62
|
// ── listFeedEntries ───────────────────────────────────────────────────────────
|
|
63
63
|
describe("listFeedEntries", () => {
|
|
64
64
|
it("returns all entries newest first", () => {
|
|
65
|
-
createFeedEntry({ type: "
|
|
65
|
+
createFeedEntry({ type: "inbox", title: "First", body: "x" });
|
|
66
66
|
createFeedEntry({ type: "notification", title: "Second", body: "y" });
|
|
67
67
|
const entries = listFeedEntries();
|
|
68
68
|
assert.equal(entries.length, 2);
|
|
@@ -70,14 +70,14 @@ describe("listFeedEntries", () => {
|
|
|
70
70
|
assert.equal(entries[1].title, "First");
|
|
71
71
|
});
|
|
72
72
|
it("filters by type=deliverable", () => {
|
|
73
|
-
createFeedEntry({ type: "
|
|
73
|
+
createFeedEntry({ type: "inbox", title: "D", body: "d" });
|
|
74
74
|
createFeedEntry({ type: "notification", title: "N", body: "n" });
|
|
75
|
-
const entries = listFeedEntries({ type: "
|
|
75
|
+
const entries = listFeedEntries({ type: "inbox" });
|
|
76
76
|
assert.equal(entries.length, 1);
|
|
77
|
-
assert.equal(entries[0].type, "
|
|
77
|
+
assert.equal(entries[0].type, "inbox");
|
|
78
78
|
});
|
|
79
79
|
it("filters by type=notification", () => {
|
|
80
|
-
createFeedEntry({ type: "
|
|
80
|
+
createFeedEntry({ type: "inbox", title: "D", body: "d" });
|
|
81
81
|
createFeedEntry({ type: "notification", title: "N", body: "n" });
|
|
82
82
|
const entries = listFeedEntries({ type: "notification" });
|
|
83
83
|
assert.equal(entries.length, 1);
|
|
@@ -108,20 +108,20 @@ describe("countUnreadFeedEntries", () => {
|
|
|
108
108
|
assert.equal(countUnreadFeedEntries(), 0);
|
|
109
109
|
});
|
|
110
110
|
it("increments on insert", () => {
|
|
111
|
-
createFeedEntry({ type: "
|
|
111
|
+
createFeedEntry({ type: "inbox", title: "T", body: "b" });
|
|
112
112
|
assert.equal(countUnreadFeedEntries(), 1);
|
|
113
113
|
createFeedEntry({ type: "notification", title: "N", body: "b" });
|
|
114
114
|
assert.equal(countUnreadFeedEntries(), 2);
|
|
115
115
|
});
|
|
116
116
|
it("decreases when marked read", () => {
|
|
117
|
-
const e = createFeedEntry({ type: "
|
|
117
|
+
const e = createFeedEntry({ type: "inbox", title: "T", body: "b" });
|
|
118
118
|
markFeedEntryRead(e.id);
|
|
119
119
|
assert.equal(countUnreadFeedEntries(), 0);
|
|
120
120
|
});
|
|
121
121
|
it("filters by type", () => {
|
|
122
|
-
createFeedEntry({ type: "
|
|
122
|
+
createFeedEntry({ type: "inbox", title: "D", body: "d" });
|
|
123
123
|
createFeedEntry({ type: "notification", title: "N", body: "n" });
|
|
124
|
-
assert.equal(countUnreadFeedEntries("
|
|
124
|
+
assert.equal(countUnreadFeedEntries("inbox"), 1);
|
|
125
125
|
assert.equal(countUnreadFeedEntries("notification"), 1);
|
|
126
126
|
});
|
|
127
127
|
});
|
|
@@ -158,11 +158,11 @@ describe("markAllFeedEntriesRead", () => {
|
|
|
158
158
|
assert.equal(markAllFeedEntriesRead(), 0);
|
|
159
159
|
});
|
|
160
160
|
it("respects type filter — only marks matching type", () => {
|
|
161
|
-
createFeedEntry({ type: "
|
|
161
|
+
createFeedEntry({ type: "inbox", title: "D", body: "d" });
|
|
162
162
|
createFeedEntry({ type: "notification", title: "N", body: "n" });
|
|
163
163
|
const count = markAllFeedEntriesRead("notification");
|
|
164
164
|
assert.equal(count, 1);
|
|
165
|
-
assert.equal(countUnreadFeedEntries("
|
|
165
|
+
assert.equal(countUnreadFeedEntries("inbox"), 1);
|
|
166
166
|
assert.equal(countUnreadFeedEntries("notification"), 0);
|
|
167
167
|
});
|
|
168
168
|
});
|
|
@@ -170,7 +170,7 @@ describe("markAllFeedEntriesRead", () => {
|
|
|
170
170
|
describe("markFeedEntriesRead", () => {
|
|
171
171
|
it("marks multiple entries read and returns change count", () => {
|
|
172
172
|
const a = createFeedEntry({ type: "notification", title: "A", body: "a" });
|
|
173
|
-
const b = createFeedEntry({ type: "
|
|
173
|
+
const b = createFeedEntry({ type: "inbox", title: "B", body: "b" });
|
|
174
174
|
const c = createFeedEntry({ type: "notification", title: "C", body: "c" });
|
|
175
175
|
const count = markFeedEntriesRead([a.id, b.id, c.id]);
|
|
176
176
|
assert.equal(count, 3);
|
|
@@ -182,7 +182,7 @@ describe("markFeedEntriesRead", () => {
|
|
|
182
182
|
assert.equal(countUnreadFeedEntries(), 1);
|
|
183
183
|
});
|
|
184
184
|
it("works correctly for a single id", () => {
|
|
185
|
-
const e = createFeedEntry({ type: "
|
|
185
|
+
const e = createFeedEntry({ type: "inbox", title: "Solo", body: "b" });
|
|
186
186
|
assert.equal(markFeedEntriesRead([e.id]), 1);
|
|
187
187
|
const entries = listFeedEntries();
|
|
188
188
|
assert.ok(entries[0].read_at !== null);
|
|
@@ -211,13 +211,13 @@ describe("deleteFeedEntry", () => {
|
|
|
211
211
|
assert.equal(deleteFeedEntry(9999), false);
|
|
212
212
|
});
|
|
213
213
|
it("returns true and removes the entry", () => {
|
|
214
|
-
const e = createFeedEntry({ type: "
|
|
214
|
+
const e = createFeedEntry({ type: "inbox", title: "T", body: "b" });
|
|
215
215
|
assert.equal(deleteFeedEntry(e.id), true);
|
|
216
216
|
const entries = listFeedEntries();
|
|
217
217
|
assert.equal(entries.find((x) => x.id === e.id), undefined);
|
|
218
218
|
});
|
|
219
219
|
it("second delete returns false (not idempotent)", () => {
|
|
220
|
-
const e = createFeedEntry({ type: "
|
|
220
|
+
const e = createFeedEntry({ type: "inbox", title: "T", body: "b" });
|
|
221
221
|
deleteFeedEntry(e.id);
|
|
222
222
|
assert.equal(deleteFeedEntry(e.id), false);
|
|
223
223
|
});
|
|
@@ -226,7 +226,7 @@ describe("deleteFeedEntry", () => {
|
|
|
226
226
|
describe("deleteFeedEntries", () => {
|
|
227
227
|
it("deletes multiple entries and returns change count", () => {
|
|
228
228
|
const a = createFeedEntry({ type: "notification", title: "A", body: "a" });
|
|
229
|
-
const b = createFeedEntry({ type: "
|
|
229
|
+
const b = createFeedEntry({ type: "inbox", title: "B", body: "b" });
|
|
230
230
|
const c = createFeedEntry({ type: "notification", title: "C", body: "c" });
|
|
231
231
|
const count = deleteFeedEntries([a.id, b.id, c.id]);
|
|
232
232
|
assert.equal(count, 3);
|
|
@@ -249,7 +249,7 @@ describe("deleteFeedEntries", () => {
|
|
|
249
249
|
assert.equal(deleteFeedEntries([9991, 9992, 9993]), 0);
|
|
250
250
|
});
|
|
251
251
|
it("mix of existing and non-existent ids — only deletes what exists", () => {
|
|
252
|
-
const e = createFeedEntry({ type: "
|
|
252
|
+
const e = createFeedEntry({ type: "inbox", title: "Real", body: "b" });
|
|
253
253
|
const count = deleteFeedEntries([e.id, 9999]);
|
|
254
254
|
assert.equal(count, 1);
|
|
255
255
|
assert.deepEqual(listFeedEntries(), []);
|
package/dist/store/squads.js
CHANGED
|
@@ -133,7 +133,7 @@ export function logDecision(squadSlug, decision, context) {
|
|
|
133
133
|
}
|
|
134
134
|
export function getDecisions(squadSlug, limit = 20) {
|
|
135
135
|
return getDb()
|
|
136
|
-
.prepare("SELECT * FROM squad_decisions WHERE squad_slug = ? ORDER BY created_at DESC LIMIT ?")
|
|
136
|
+
.prepare("SELECT * FROM squad_decisions WHERE squad_slug = ? ORDER BY created_at DESC, id DESC LIMIT ?")
|
|
137
137
|
.all(squadSlug, limit);
|
|
138
138
|
}
|
|
139
139
|
export function getDecisionsSummary(squadSlug) {
|