chapterhouse 0.4.2 → 0.4.3
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/copilot/agents.js +3 -2
- package/dist/copilot/orchestrator.js +48 -10
- package/dist/copilot/orchestrator.test.js +59 -14
- package/dist/copilot/tools.js +183 -7
- package/dist/copilot/tools.memory.test.js +125 -1
- package/dist/daemon.js +6 -0
- package/dist/memory/action-items.js +100 -0
- package/dist/memory/action-items.test.js +83 -0
- package/dist/memory/eot.js +28 -3
- package/dist/memory/eot.test.js +108 -0
- package/dist/memory/hot-tier.js +60 -1
- package/dist/memory/hot-tier.test.js +38 -0
- package/dist/memory/housekeeping-scheduler.js +152 -0
- package/dist/memory/housekeeping-scheduler.test.js +187 -0
- package/dist/memory/index.js +1 -0
- package/dist/memory/recall.js +59 -0
- package/dist/memory/recall.test.js +27 -0
- package/dist/memory/tiering.js +33 -3
- package/dist/store/db.js +103 -16
- package/dist/store/db.test.js +61 -5
- package/package.json +1 -1
- package/web/dist/assets/index-BTI_m0OE.css +10 -0
- package/web/dist/assets/{index-B_cCSHan.js → index-D4-uRAi6.js} +4 -4
- package/web/dist/assets/index-D4-uRAi6.js.map +1 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-B_cCSHan.js.map +0 -1
- package/web/dist/assets/index-DhY5yWmC.css +0 -10
package/dist/store/db.js
CHANGED
|
@@ -141,7 +141,7 @@ function rebuildMemoryTierTables(database) {
|
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
function ensureMemoryTierColumns(database) {
|
|
144
|
-
for (const table of ["mem_entities", "mem_observations", "mem_decisions"]) {
|
|
144
|
+
for (const table of ["mem_entities", "mem_observations", "mem_decisions", "mem_action_items"]) {
|
|
145
145
|
if (!hasColumn(database, table, "tier")) {
|
|
146
146
|
database.exec(`ALTER TABLE ${table} ADD COLUMN tier TEXT DEFAULT 'warm'`);
|
|
147
147
|
}
|
|
@@ -184,12 +184,24 @@ function ensureMemoryTierColumns(database) {
|
|
|
184
184
|
ELSE 'warm'
|
|
185
185
|
END
|
|
186
186
|
`);
|
|
187
|
+
database.exec(`
|
|
188
|
+
UPDATE mem_action_items
|
|
189
|
+
SET tier = CASE
|
|
190
|
+
WHEN status IN ('done', 'dropped') THEN 'cold'
|
|
191
|
+
WHEN tier = 'glacier' THEN 'cold'
|
|
192
|
+
WHEN tier IN ('hot', 'warm', 'cold') THEN tier
|
|
193
|
+
WHEN status = 'open' AND due_at IS NOT NULL AND datetime(due_at) <= datetime('now', '+7 days') THEN 'hot'
|
|
194
|
+
ELSE 'warm'
|
|
195
|
+
END
|
|
196
|
+
`);
|
|
187
197
|
}
|
|
188
198
|
function ensureMemoryIndexes(database) {
|
|
189
199
|
database.exec(`CREATE INDEX IF NOT EXISTS mem_entities_scope_kind_idx ON mem_entities(scope_id, kind)`);
|
|
190
200
|
database.exec(`CREATE UNIQUE INDEX IF NOT EXISTS mem_entities_scope_kind_name_idx ON mem_entities(scope_id, kind, name)`);
|
|
191
201
|
database.exec(`CREATE INDEX IF NOT EXISTS mem_observations_scope_idx ON mem_observations(scope_id)`);
|
|
192
202
|
database.exec(`CREATE INDEX IF NOT EXISTS mem_decisions_scope_idx ON mem_decisions(scope_id)`);
|
|
203
|
+
database.exec(`CREATE INDEX IF NOT EXISTS idx_mem_action_items_scope_status ON mem_action_items(scope_id, status)`);
|
|
204
|
+
database.exec(`CREATE INDEX IF NOT EXISTS idx_mem_action_items_due ON mem_action_items(status, due_at)`);
|
|
193
205
|
}
|
|
194
206
|
const MEMORY_SCOPE_SEEDS = [
|
|
195
207
|
{
|
|
@@ -561,6 +573,8 @@ export function getDb() {
|
|
|
561
573
|
source TEXT NOT NULL DEFAULT 'unknown',
|
|
562
574
|
session_key TEXT NOT NULL DEFAULT 'default',
|
|
563
575
|
turn_id TEXT,
|
|
576
|
+
agent_slug TEXT,
|
|
577
|
+
agent_display_name TEXT,
|
|
564
578
|
run_id TEXT,
|
|
565
579
|
ts DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
566
580
|
)
|
|
@@ -587,6 +601,8 @@ export function getDb() {
|
|
|
587
601
|
const oldConvColNames = new Set(oldConvCols.map((column) => column.name));
|
|
588
602
|
const sessionKeySelect = oldConvColNames.has("session_key") ? "session_key" : "'default'";
|
|
589
603
|
const turnIdSelect = oldConvColNames.has("turn_id") ? "turn_id" : "NULL";
|
|
604
|
+
const agentSlugSelect = oldConvColNames.has("agent_slug") ? "agent_slug" : "NULL";
|
|
605
|
+
const agentDisplayNameSelect = oldConvColNames.has("agent_display_name") ? "agent_display_name" : "NULL";
|
|
590
606
|
const runIdSelect = oldConvColNames.has("run_id") ? "run_id" : "NULL";
|
|
591
607
|
db.exec(`
|
|
592
608
|
CREATE TABLE conversation_log (
|
|
@@ -596,13 +612,15 @@ export function getDb() {
|
|
|
596
612
|
source TEXT NOT NULL DEFAULT 'unknown',
|
|
597
613
|
session_key TEXT NOT NULL DEFAULT 'default',
|
|
598
614
|
turn_id TEXT,
|
|
615
|
+
agent_slug TEXT,
|
|
616
|
+
agent_display_name TEXT,
|
|
599
617
|
run_id TEXT,
|
|
600
618
|
ts DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
601
619
|
)
|
|
602
620
|
`);
|
|
603
621
|
db.exec(`
|
|
604
|
-
INSERT INTO conversation_log (role, content, source, session_key, turn_id, run_id, ts)
|
|
605
|
-
SELECT role, content, source, ${sessionKeySelect}, ${turnIdSelect}, ${runIdSelect}, ts FROM conversation_log_old
|
|
622
|
+
INSERT INTO conversation_log (role, content, source, session_key, turn_id, agent_slug, agent_display_name, run_id, ts)
|
|
623
|
+
SELECT role, content, source, ${sessionKeySelect}, ${turnIdSelect}, ${agentSlugSelect}, ${agentDisplayNameSelect}, ${runIdSelect}, ts FROM conversation_log_old
|
|
606
624
|
`);
|
|
607
625
|
db.exec(`DROP TABLE conversation_log_old`);
|
|
608
626
|
}
|
|
@@ -617,7 +635,7 @@ export function getDb() {
|
|
|
617
635
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
618
636
|
)
|
|
619
637
|
`);
|
|
620
|
-
// Migrate: add
|
|
638
|
+
// Migrate: add metadata columns to conversation_log if not present
|
|
621
639
|
const convCols = db.prepare(`PRAGMA table_info(conversation_log)`).all();
|
|
622
640
|
if (!convCols.some((c) => c.name === 'session_key')) {
|
|
623
641
|
db.exec(`ALTER TABLE conversation_log ADD COLUMN session_key TEXT NOT NULL DEFAULT 'default'`);
|
|
@@ -625,6 +643,12 @@ export function getDb() {
|
|
|
625
643
|
if (!convCols.some((c) => c.name === 'turn_id')) {
|
|
626
644
|
db.exec(`ALTER TABLE conversation_log ADD COLUMN turn_id TEXT`);
|
|
627
645
|
}
|
|
646
|
+
if (!convCols.some((c) => c.name === 'agent_slug')) {
|
|
647
|
+
db.exec(`ALTER TABLE conversation_log ADD COLUMN agent_slug TEXT`);
|
|
648
|
+
}
|
|
649
|
+
if (!convCols.some((c) => c.name === 'agent_display_name')) {
|
|
650
|
+
db.exec(`ALTER TABLE conversation_log ADD COLUMN agent_display_name TEXT`);
|
|
651
|
+
}
|
|
628
652
|
if (!convCols.some((c) => c.name === "run_id")) {
|
|
629
653
|
db.exec(`ALTER TABLE conversation_log ADD COLUMN run_id TEXT`);
|
|
630
654
|
}
|
|
@@ -785,6 +809,27 @@ export function getDb() {
|
|
|
785
809
|
last_recalled_at DATETIME,
|
|
786
810
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
787
811
|
)
|
|
812
|
+
`);
|
|
813
|
+
db.exec(`
|
|
814
|
+
CREATE TABLE IF NOT EXISTS mem_action_items (
|
|
815
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
816
|
+
scope_id INTEGER NOT NULL REFERENCES mem_scopes(id),
|
|
817
|
+
entity_id INTEGER REFERENCES mem_entities(id),
|
|
818
|
+
title TEXT NOT NULL,
|
|
819
|
+
detail TEXT,
|
|
820
|
+
status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open', 'done', 'dropped', 'snoozed')),
|
|
821
|
+
due_at TEXT,
|
|
822
|
+
snooze_until TEXT,
|
|
823
|
+
source TEXT,
|
|
824
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
825
|
+
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
826
|
+
resolved_at TEXT,
|
|
827
|
+
resolution_reason TEXT,
|
|
828
|
+
tier TEXT NOT NULL DEFAULT 'warm' CHECK(tier IN ('hot', 'warm', 'cold')),
|
|
829
|
+
tier_pinned_at TEXT,
|
|
830
|
+
tier_reason TEXT,
|
|
831
|
+
last_recalled_at TEXT
|
|
832
|
+
)
|
|
788
833
|
`);
|
|
789
834
|
const decisionCols = db.prepare(`PRAGMA table_info(mem_decisions)`).all();
|
|
790
835
|
if (!decisionCols.some((column) => column.name === "superseded_by")) {
|
|
@@ -856,6 +901,13 @@ export function getDb() {
|
|
|
856
901
|
rationale,
|
|
857
902
|
content_rowid='id'
|
|
858
903
|
)
|
|
904
|
+
`);
|
|
905
|
+
db.exec(`
|
|
906
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS mem_action_items_fts USING fts5(
|
|
907
|
+
title,
|
|
908
|
+
detail,
|
|
909
|
+
content_rowid='id'
|
|
910
|
+
)
|
|
859
911
|
`);
|
|
860
912
|
// Sync triggers
|
|
861
913
|
db.exec(`DROP TRIGGER IF EXISTS memories_ai`);
|
|
@@ -867,6 +919,9 @@ export function getDb() {
|
|
|
867
919
|
db.exec(`DROP TRIGGER IF EXISTS mem_decisions_ai`);
|
|
868
920
|
db.exec(`DROP TRIGGER IF EXISTS mem_decisions_ad`);
|
|
869
921
|
db.exec(`DROP TRIGGER IF EXISTS mem_decisions_au`);
|
|
922
|
+
db.exec(`DROP TRIGGER IF EXISTS mem_action_items_ai`);
|
|
923
|
+
db.exec(`DROP TRIGGER IF EXISTS mem_action_items_ad`);
|
|
924
|
+
db.exec(`DROP TRIGGER IF EXISTS mem_action_items_au`);
|
|
870
925
|
db.exec(`
|
|
871
926
|
CREATE TRIGGER memories_ai AFTER INSERT ON memories BEGIN
|
|
872
927
|
INSERT INTO memories_fts(rowid, content) VALUES (new.id, new.content);
|
|
@@ -916,6 +971,24 @@ export function getDb() {
|
|
|
916
971
|
INSERT INTO mem_decisions_fts(rowid, title, rationale)
|
|
917
972
|
VALUES (new.id, new.title, new.rationale);
|
|
918
973
|
END
|
|
974
|
+
`);
|
|
975
|
+
db.exec(`
|
|
976
|
+
CREATE TRIGGER mem_action_items_ai AFTER INSERT ON mem_action_items BEGIN
|
|
977
|
+
INSERT INTO mem_action_items_fts(rowid, title, detail)
|
|
978
|
+
VALUES (new.id, new.title, new.detail);
|
|
979
|
+
END
|
|
980
|
+
`);
|
|
981
|
+
db.exec(`
|
|
982
|
+
CREATE TRIGGER mem_action_items_ad AFTER DELETE ON mem_action_items BEGIN
|
|
983
|
+
DELETE FROM mem_action_items_fts WHERE rowid = old.id;
|
|
984
|
+
END
|
|
985
|
+
`);
|
|
986
|
+
db.exec(`
|
|
987
|
+
CREATE TRIGGER mem_action_items_au AFTER UPDATE ON mem_action_items BEGIN
|
|
988
|
+
DELETE FROM mem_action_items_fts WHERE rowid = old.id;
|
|
989
|
+
INSERT INTO mem_action_items_fts(rowid, title, detail)
|
|
990
|
+
VALUES (new.id, new.title, new.detail);
|
|
991
|
+
END
|
|
919
992
|
`);
|
|
920
993
|
// Backfill: check if FTS is in sync by comparing row counts
|
|
921
994
|
const memCount = db.prepare(`SELECT COUNT(*) as c FROM memories`).get().c;
|
|
@@ -933,6 +1006,11 @@ export function getDb() {
|
|
|
933
1006
|
if (decisionCount > 0 && decisionFtsCount < decisionCount) {
|
|
934
1007
|
db.exec(`INSERT INTO mem_decisions_fts(mem_decisions_fts) VALUES ('rebuild')`);
|
|
935
1008
|
}
|
|
1009
|
+
const actionItemCount = db.prepare(`SELECT COUNT(*) as c FROM mem_action_items`).get().c;
|
|
1010
|
+
const actionItemFtsCount = db.prepare(`SELECT COUNT(*) as c FROM mem_action_items_fts`).get().c;
|
|
1011
|
+
if (actionItemCount > 0 && actionItemFtsCount < actionItemCount) {
|
|
1012
|
+
db.exec(`INSERT INTO mem_action_items_fts(mem_action_items_fts) VALUES ('rebuild')`);
|
|
1013
|
+
}
|
|
936
1014
|
fts5Available = true;
|
|
937
1015
|
}
|
|
938
1016
|
catch {
|
|
@@ -961,10 +1039,10 @@ export function deleteState(key) {
|
|
|
961
1039
|
db.prepare(`DELETE FROM max_state WHERE key = ?`).run(key);
|
|
962
1040
|
}
|
|
963
1041
|
/** Log a conversation turn (user, assistant, or system). */
|
|
964
|
-
export function logConversation(role, content, source, sessionKey = "default",
|
|
1042
|
+
export function logConversation(role, content, source, sessionKey = "default", metadata) {
|
|
965
1043
|
const db = getDb();
|
|
966
|
-
db.prepare(`INSERT INTO conversation_log (role, content, source, session_key, turn_id,
|
|
967
|
-
|
|
1044
|
+
db.prepare(`INSERT INTO conversation_log (role, content, source, session_key, turn_id, agent_slug, agent_display_name, run_id)
|
|
1045
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(role, content, source, sessionKey, metadata?.turnId ?? null, metadata?.agentSlug ?? null, metadata?.agentDisplayName ?? null, getCurrentRunId());
|
|
968
1046
|
// Keep last 1000 entries to support context recovery after session loss
|
|
969
1047
|
logInsertCount++;
|
|
970
1048
|
if (logInsertCount % 50 === 0) {
|
|
@@ -1076,24 +1154,33 @@ export function getSessionMessages(sessionKey, limit, options = {}) {
|
|
|
1076
1154
|
const runId = options.runId ?? getCurrentRunId();
|
|
1077
1155
|
const rows = includeHistorical
|
|
1078
1156
|
? db
|
|
1079
|
-
.prepare(`SELECT id, role, content, ts, turn_id FROM conversation_log
|
|
1157
|
+
.prepare(`SELECT id, role, content, ts, turn_id, agent_slug, agent_display_name FROM conversation_log
|
|
1080
1158
|
WHERE session_key = ? AND role IN ('user', 'assistant', 'agent_completion')
|
|
1081
1159
|
ORDER BY id DESC LIMIT ?`)
|
|
1082
1160
|
.all(sessionKey, effectiveLimit)
|
|
1083
1161
|
: db
|
|
1084
|
-
.prepare(`SELECT id, role, content, ts, turn_id FROM conversation_log
|
|
1162
|
+
.prepare(`SELECT id, role, content, ts, turn_id, agent_slug, agent_display_name FROM conversation_log
|
|
1085
1163
|
WHERE session_key = ? AND run_id = ? AND role IN ('user', 'assistant', 'agent_completion')
|
|
1086
1164
|
ORDER BY id DESC LIMIT ?`)
|
|
1087
1165
|
.all(sessionKey, runId, effectiveLimit);
|
|
1088
1166
|
// Reverse so oldest is first (chronological order for the UI)
|
|
1089
1167
|
rows.reverse();
|
|
1090
|
-
return rows.map((r) =>
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1168
|
+
return rows.map((r) => {
|
|
1169
|
+
const message = {
|
|
1170
|
+
id: r.id,
|
|
1171
|
+
role: r.role === "agent_completion" ? "assistant" : r.role,
|
|
1172
|
+
content: r.content,
|
|
1173
|
+
ts: normalizeSqliteTsToIso(r.ts),
|
|
1174
|
+
turn_id: r.turn_id,
|
|
1175
|
+
};
|
|
1176
|
+
if (r.turn_id)
|
|
1177
|
+
message.turnId = r.turn_id;
|
|
1178
|
+
if (r.agent_slug)
|
|
1179
|
+
message.agentSlug = r.agent_slug;
|
|
1180
|
+
if (r.agent_display_name)
|
|
1181
|
+
message.agentDisplayName = r.agent_display_name;
|
|
1182
|
+
return message;
|
|
1183
|
+
});
|
|
1097
1184
|
}
|
|
1098
1185
|
/**
|
|
1099
1186
|
* Append one event to agent_task_events and return the new event.
|
package/dist/store/db.test.js
CHANGED
|
@@ -58,6 +58,53 @@ test("getDb initializes schema, state helpers, and conversation formatting", asy
|
|
|
58
58
|
dbModule.closeDb();
|
|
59
59
|
}
|
|
60
60
|
});
|
|
61
|
+
test("getDb initializes action-item memory schema and FTS shadow", async () => {
|
|
62
|
+
const dbModule = await loadDbModule();
|
|
63
|
+
try {
|
|
64
|
+
const db = dbModule.getDb();
|
|
65
|
+
const tables = db.prepare(`SELECT name FROM sqlite_master WHERE type = 'table'`).all();
|
|
66
|
+
const tableNames = new Set(tables.map((row) => row.name));
|
|
67
|
+
assert.equal(tableNames.has("mem_action_items"), true, "expected mem_action_items table");
|
|
68
|
+
assert.equal(tableNames.has("mem_action_items_fts"), true, "expected mem_action_items_fts virtual table");
|
|
69
|
+
const columns = db.prepare(`PRAGMA table_info(mem_action_items)`).all();
|
|
70
|
+
const columnNames = new Set(columns.map((column) => column.name));
|
|
71
|
+
for (const name of [
|
|
72
|
+
"id",
|
|
73
|
+
"scope_id",
|
|
74
|
+
"entity_id",
|
|
75
|
+
"title",
|
|
76
|
+
"detail",
|
|
77
|
+
"status",
|
|
78
|
+
"due_at",
|
|
79
|
+
"snooze_until",
|
|
80
|
+
"source",
|
|
81
|
+
"created_at",
|
|
82
|
+
"updated_at",
|
|
83
|
+
"resolved_at",
|
|
84
|
+
"resolution_reason",
|
|
85
|
+
"tier",
|
|
86
|
+
"tier_pinned_at",
|
|
87
|
+
"tier_reason",
|
|
88
|
+
"last_recalled_at",
|
|
89
|
+
]) {
|
|
90
|
+
assert.equal(columnNames.has(name), true, `expected mem_action_items.${name}`);
|
|
91
|
+
}
|
|
92
|
+
const scope = db.prepare(`SELECT id FROM mem_scopes WHERE slug = 'chapterhouse'`).get();
|
|
93
|
+
const inserted = db.prepare(`
|
|
94
|
+
INSERT INTO mem_action_items (scope_id, title, detail, source)
|
|
95
|
+
VALUES (?, 'Action FTS sentinel', 'Searchable migration reminder', 'test')
|
|
96
|
+
`).run(scope.id);
|
|
97
|
+
const ftsHits = db.prepare(`
|
|
98
|
+
SELECT rowid
|
|
99
|
+
FROM mem_action_items_fts
|
|
100
|
+
WHERE mem_action_items_fts MATCH 'migration'
|
|
101
|
+
`).all();
|
|
102
|
+
assert.equal(ftsHits.some((hit) => hit.rowid === Number(inserted.lastInsertRowid)), true);
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
dbModule.closeDb();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
61
108
|
test("getDb migrates legacy conversation_log tables to allow system messages", async () => {
|
|
62
109
|
const seedDb = new Database(dbPath);
|
|
63
110
|
seedDb.exec(`
|
|
@@ -559,12 +606,15 @@ test("getSessionMessages returns empty array for unknown session", async () => {
|
|
|
559
606
|
test("getSessionMessages returns structured messages in chronological order, includes agent completions, excludes system rows, respects limit", async () => {
|
|
560
607
|
const dbModule = await loadDbModule();
|
|
561
608
|
try {
|
|
562
|
-
|
|
609
|
+
dbModule.getDb();
|
|
563
610
|
dbModule.logConversation("user", "hello", "web", "test-session");
|
|
564
611
|
dbModule.logConversation("assistant", "hi there", "web", "test-session");
|
|
565
612
|
dbModule.logConversation("system", "system noise", "worker", "test-session");
|
|
566
|
-
|
|
567
|
-
|
|
613
|
+
dbModule.logConversation("agent_completion", "Done", "background", "test-session", {
|
|
614
|
+
agentSlug: "coder",
|
|
615
|
+
agentDisplayName: "Kaylee",
|
|
616
|
+
turnId: "agent-turn-1",
|
|
617
|
+
});
|
|
568
618
|
dbModule.logConversation("user", "second message", "web", "test-session");
|
|
569
619
|
dbModule.logConversation("user", "from other session", "web", "other-session");
|
|
570
620
|
const all = dbModule.getSessionMessages("test-session");
|
|
@@ -574,14 +624,19 @@ test("getSessionMessages returns structured messages in chronological order, inc
|
|
|
574
624
|
assert.equal(all[1].role, "assistant");
|
|
575
625
|
assert.equal(all[1].content, "hi there");
|
|
576
626
|
assert.equal(all[2].role, "assistant");
|
|
577
|
-
assert.equal(all[2].content, "
|
|
627
|
+
assert.equal(all[2].content, "Done");
|
|
628
|
+
assert.equal(all[2].agentSlug, "coder");
|
|
629
|
+
assert.equal(all[2].agentDisplayName, "Kaylee");
|
|
630
|
+
assert.equal(all[2].turnId, "agent-turn-1");
|
|
578
631
|
assert.equal(all[3].role, "user");
|
|
579
632
|
assert.equal(all[3].content, "second message");
|
|
580
633
|
// Limit clamping
|
|
581
634
|
const limited = dbModule.getSessionMessages("test-session", 2);
|
|
582
635
|
assert.equal(limited.length, 2, "limit=2 returns 2 most recent rows");
|
|
583
636
|
// After reversal, these should be the 2 most-recent renderable rows.
|
|
584
|
-
assert.equal(limited[0].content, "
|
|
637
|
+
assert.equal(limited[0].content, "Done");
|
|
638
|
+
assert.equal(limited[0].agentSlug, "coder");
|
|
639
|
+
assert.equal(limited[0].turnId, "agent-turn-1");
|
|
585
640
|
assert.equal(limited[1].content, "second message");
|
|
586
641
|
// Other session not leaked
|
|
587
642
|
const other = dbModule.getSessionMessages("other-session");
|
|
@@ -605,6 +660,7 @@ test("getSessionMessages returns stable row id and turn_id for hydration reconci
|
|
|
605
660
|
const message = messages[0];
|
|
606
661
|
assert.equal(message.id, Number(result.lastInsertRowid));
|
|
607
662
|
assert.equal(message.turn_id, "turn-stable-1");
|
|
663
|
+
assert.equal(message.turnId, "turn-stable-1");
|
|
608
664
|
}
|
|
609
665
|
finally {
|
|
610
666
|
dbModule.closeDb();
|
package/package.json
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
|
|
2
|
+
Theme: GitHub Dark
|
|
3
|
+
Description: Dark theme as seen on github.com
|
|
4
|
+
Author: github.com
|
|
5
|
+
Maintainer: @Hirse
|
|
6
|
+
Updated: 2021-05-15
|
|
7
|
+
|
|
8
|
+
Outdated base version: https://github.com/primer/github-syntax-dark
|
|
9
|
+
Current colors taken from GitHub's CSS
|
|
10
|
+
*/.hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-variable,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id{color:#79c0ff}.hljs-regexp,.hljs-string,.hljs-meta .hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-comment,.hljs-code,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-tag,.hljs-selector-pseudo{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}:root{color-scheme:dark;--bg: #0e1116;--bg-elev: #161b22;--bg-elev-2: #21262d;--fg: #e6edf3;--fg-dim: #8b949e;--border: #30363d;--accent: #3b82f6;--accent-fg: #ffffff;--danger: #f87171;--user-bubble: #1e293b}*{box-sizing:border-box}html,body,#root{height:100%;margin:0}body{background:var(--bg);color:var(--fg);font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,sans-serif;font-size:14px;line-height:1.5}a,.link{color:var(--accent);text-decoration:none}a:hover,.link:hover{text-decoration:underline}button,input,textarea,select{font:inherit}button:focus-visible,a:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible{outline:2px solid var(--accent);outline-offset:2px}code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.9em;background:var(--bg-elev-2);padding:1px 5px;border-radius:4px}.dim{color:var(--fg-dim)}.small{font-size:12px}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.skip-link{position:absolute;left:16px;top:-48px;z-index:10;padding:10px 14px;border-radius:8px;background:var(--accent);color:var(--accent-fg)}.skip-link:focus{top:16px}.layout{display:grid;grid-template-columns:220px 1fr;height:100%}.sidebar{background:var(--bg-elev);border-right:1px solid var(--border);padding:16px 0;display:flex;flex-direction:column}.sidebar-brand{display:flex;align-items:center;gap:10px;padding:0 18px 18px;font-weight:600;font-size:16px;border-bottom:1px solid var(--border);margin-bottom:12px}.sidebar nav{display:flex;flex-direction:column}.nav-link{padding:9px 18px;color:var(--fg);border-left:2px solid transparent}.nav-link:hover{background:var(--bg-elev-2);text-decoration:none}.nav-link.active{background:var(--bg-elev-2);border-left-color:var(--accent);color:var(--fg)}.nav-group{display:flex;flex-direction:column}.nav-group-row{display:flex;align-items:stretch}.nav-group-label{flex:1}.nav-group-toggle{background:none;border:none;cursor:pointer;padding:0 14px 0 4px;color:var(--fg-muted, var(--fg));display:flex;align-items:center;justify-content:center;border-left:2px solid transparent}.nav-group-toggle:hover{background:var(--bg-elev-2)}.nav-chevron{display:inline-block;font-size:18px;line-height:1;transition:transform .18s ease;transform:rotate(0)}.nav-chevron-open{transform:rotate(90deg)}.nav-recents{list-style:none;margin:0;padding:0}.nav-recent-link{display:flex;align-items:baseline;justify-content:space-between;gap:6px;width:100%;background:none;border:none;border-left:2px solid transparent;padding:6px 18px 6px 28px;cursor:pointer;color:var(--fg);text-align:left;font-size:13px}.nav-recent-link:hover{background:var(--bg-elev-2);text-decoration:none;border-left-color:var(--accent)}.nav-recent-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}.nav-recent-hint{font-size:11px;flex-shrink:0;opacity:.6}.nav-recents-empty{padding:4px 28px 6px;font-size:12px;margin:0}.main{overflow:hidden;display:flex;flex-direction:column}.app-header{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px 24px;border-bottom:1px solid var(--border);background:var(--bg-elev)}.app-header-title{font-size:16px;font-weight:600;margin:0}.app-header-title-row{display:inline-flex;align-items:center;gap:10px}.app-header-user{color:var(--fg-dim);font-size:13px}.mode-badge{display:inline-flex;align-items:center;border-radius:999px;padding:3px 10px;font-size:12px;font-weight:600;line-height:1;border:1px solid transparent}.mode-standalone{background:var(--bg-elev-2);border-color:var(--border);color:var(--fg-dim)}.mode-team{background:color-mix(in srgb,var(--accent) 14%,transparent);border-color:color-mix(in srgb,var(--accent) 35%,var(--border));color:var(--accent)}.sse-badge{display:inline-flex;align-items:center;gap:6px;border-radius:999px;padding:3px 10px;font-size:12px;font-weight:600;line-height:1;border:1px solid transparent}.sse-badge__dot{display:inline-block;width:7px;height:7px;border-radius:50%;flex-shrink:0}.sse-badge--reconnecting{background:color-mix(in srgb,#f59e0b 12%,transparent);border-color:color-mix(in srgb,#f59e0b 35%,var(--border));color:#b45309}.sse-badge--reconnecting .sse-badge__dot{background:#f59e0b;animation:sse-pulse 1.2s ease-in-out infinite}.sse-badge--disconnected{background:color-mix(in srgb,#ef4444 12%,transparent);border-color:color-mix(in srgb,#ef4444 35%,var(--border));color:#b91c1c}.sse-badge--disconnected .sse-badge__dot{background:#ef4444}.sse-badge__reconnect-btn{background:none;border:none;padding:0;margin-left:4px;font-size:12px;font-weight:600;color:inherit;cursor:pointer;text-decoration:underline;text-underline-offset:2px}.sse-badge__reconnect-btn:hover{opacity:.8}@keyframes sse-pulse{0%,to{opacity:1}50%{opacity:.35}}max-width: 760px; margin: 0 auto; padding: 32px; } .loading,.empty-state{padding:32px;color:var(--fg-dim)}.empty-state h2{color:var(--fg);margin-top:0;margin-bottom:8px}.empty-state p{margin:0 0 12px}.empty-state-icon{font-size:28px;margin-bottom:8px;line-height:1}.empty-state-action{margin-top:4px}.auth-screen{min-height:100%;display:grid;place-items:center;padding:32px}.auth-card{width:min(420px,100%);background:var(--bg-elev);border:1px solid var(--border);border-radius:12px;padding:24px}.auth-card h1{margin-top:0;margin-bottom:8px}.auth-card p{margin-top:0;margin-bottom:20px;color:var(--fg-dim)}.page{padding:24px 32px;overflow:auto;flex:1;min-width:0}.page-header{margin-bottom:16px}.page-header h1{margin:0 0 4px;font-size:22px}.projects-table-wrap{overflow-x:auto}.projects-table{width:100%;border-collapse:collapse;background:var(--bg-elev);border:1px solid var(--border);border-radius:12px;overflow:hidden}.projects-table th,.projects-table td{padding:12px 14px;border-bottom:1px solid var(--border);text-align:left;vertical-align:top}.projects-table thead th{background:var(--bg-elev-2);font-size:12px;text-transform:uppercase;letter-spacing:.04em;color:var(--fg-dim)}.projects-table tbody tr:last-child th,.projects-table tbody tr:last-child td{border-bottom:0}.project-detail-backlink{display:inline-flex;align-items:center;gap:6px;margin-bottom:12px;font-size:13px}.project-detail-summary{margin:0;color:var(--fg-dim)}.project-detail-layout{display:grid;gap:16px}.project-detail-card{background:var(--bg-elev);border:1px solid var(--border);border-radius:12px;padding:18px}.project-detail-card h2{margin:0 0 14px;font-size:16px}.project-detail-meta{display:grid;grid-template-columns:minmax(120px,180px) 1fr;gap:10px 16px;margin:0}.project-detail-meta dt{color:var(--fg-dim);font-weight:600}.project-detail-meta dd{margin:0}.project-detail-actions{display:flex;justify-content:flex-end;margin-top:16px}.project-hard-rules{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px;margin:0}.project-hard-rule{background:var(--bg-elev-2);border:1px solid var(--border);border-radius:8px;padding:12px 14px}.project-hard-rule dt{color:var(--fg-dim);font-size:12px;font-weight:600;letter-spacing:.03em;text-transform:uppercase}.project-hard-rule dd{margin:6px 0 0;font-size:14px}.project-hard-rules-form{display:grid;gap:14px}.project-hard-rule-input{display:grid;gap:10px;align-content:start}.project-hard-rule-label{color:var(--fg-dim);font-size:12px;font-weight:600;letter-spacing:.03em;text-transform:uppercase}.project-hard-rule input[type=checkbox]{width:16px;height:16px}.project-hard-rule-text-input{background:var(--bg-elev);border:1px solid var(--border);border-radius:6px;color:var(--fg);font-size:13px;padding:6px 10px;width:100%}.project-hard-rule-text-input:focus{outline:none;border-color:var(--accent)}.project-soft-rules-form{display:grid;gap:12px}.project-soft-rules-grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));align-items:start}.project-soft-rules-editor,.project-soft-rules-preview-card{min-width:0}.project-soft-rules-label{color:var(--fg-dim);font-size:12px;font-weight:600;letter-spacing:.03em;text-transform:uppercase}.project-soft-rules-input{background:var(--bg-elev);border:1px solid var(--border);border-radius:6px;color:var(--fg);font-size:13px;line-height:1.5;min-height:140px;padding:10px 12px;resize:vertical;width:100%}.project-soft-rules-input:focus{outline:none;border-color:var(--accent)}.project-soft-rules-hint{color:var(--fg-dim);font-size:12px;margin:0}.project-soft-rules-preview-card{background:var(--bg-elev-2);border:1px solid var(--border);border-radius:8px;padding:12px 14px}.project-soft-rules-preview-title{margin:0 0 10px;color:var(--fg-dim);font-size:12px;font-weight:600;letter-spacing:.03em;text-transform:uppercase}.project-soft-rules-preview{min-height:140px}.project-soft-rules-preview .md>:first-child{margin-top:0}.project-soft-rules-preview .md>:last-child{margin-bottom:0}.project-save-feedback{margin:0;font-size:12px}.project-save-feedback-success{color:#4ade80}.project-save-feedback-error{color:var(--danger)}.project-detail-empty{padding:0}.projects-count{width:120px}.error-notice{background:#f871711a;border:1px solid var(--danger);color:var(--danger);padding:12px 14px;border-radius:8px;margin-bottom:16px}.error-notice.inline{margin-bottom:12px}.error-notice-title{margin:0 0 4px;font-size:16px}.error-notice-message{margin:0}.error-notice-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}.error-details{background:var(--bg-elev-2);padding:12px;border-radius:8px;overflow:auto}.loading-state{display:flex;align-items:flex-start;gap:12px;padding:16px 0;color:var(--fg-dim)}.loading-state.inline{padding:10px 0}.loading-state.centered{justify-content:center;padding:48px 32px}.loading-spinner{width:18px;height:18px;border:2px solid rgba(59,130,246,.25);border-top-color:var(--accent);border-radius:999px;flex:none;margin-top:2px;animation:spin .9s linear infinite}.loading-state-label{color:var(--fg);font-weight:500}.loading-state-detail{margin-top:2px}.btn{background:var(--bg-elev-2);color:var(--fg);border:1px solid var(--border);border-radius:6px;padding:6px 14px;font-size:13px;cursor:pointer}.btn:hover{background:var(--bg-elev)}.btn.primary{background:var(--accent);color:var(--accent-fg);border-color:var(--accent)}.btn.primary:hover{filter:brightness(1.1)}.btn.danger{border-color:var(--danger);color:var(--danger)}.btn.cancel{background:var(--danger);border-color:var(--danger);color:var(--accent-fg)}.chat{display:flex;flex-direction:column;height:100%}.chat-scroll{flex:1;overflow:auto;padding:24px 32px 0}.chat-log{display:flex;flex-direction:column}.turn-wrapper{margin-bottom:18px}.turn-wrapper+.turn-wrapper:not(.has-separator) .bubble{border-top:1px solid var(--border);padding-top:14px}.turn-header{display:flex;align-items:center;gap:8px;margin-bottom:6px;font-size:12px}.turn-header--user{justify-content:flex-end}.turn-actor-badge{display:inline-flex;align-items:center;gap:4px;background:var(--bg-elev);border:1px solid var(--border);border-radius:999px;padding:2px 8px;font-size:11px;font-weight:500;-webkit-user-select:none;user-select:none}.turn-header--agent .turn-actor-badge{background:color-mix(in srgb,var(--accent) 10%,var(--bg-elev));border-color:color-mix(in srgb,var(--accent) 35%,var(--border))}.turn-actor-icon{font-size:11px;line-height:1}.turn-agent-slug{color:var(--accent);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:10px;font-weight:600}.turn-ts{font-size:11px;color:var(--fg-dim);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;cursor:default;-webkit-user-select:none;user-select:none}.time-separator{display:flex;align-items:center;gap:10px;margin:16px 0 10px;color:var(--fg-dim);font-size:11px;font-style:italic;-webkit-user-select:none;user-select:none}.time-separator-line{flex:1;height:1px;background:var(--border);opacity:.5}.time-separator-label{white-space:nowrap;opacity:.7;letter-spacing:.02em}.bubble{max-width:800px}.bubble.user{margin-left:auto;text-align:right}.bubble--agent-activity{opacity:.88;border-left:2px solid var(--accent, #6366f1);padding-left:10px}.bubble.user .user-text{display:inline-block;background:var(--user-bubble);border:1px solid var(--border);padding:8px 14px;border-radius:14px;white-space:pre-wrap;text-align:left;margin:0}.route-tag{font-size:11px;color:var(--fg-dim);margin-top:4px}.copy-btn-wrap{position:relative}.copy-btn{position:absolute;top:6px;right:6px;display:flex;align-items:center;justify-content:center;padding:4px;background:var(--bg-elev);border:1px solid var(--border);border-radius:6px;color:var(--fg-dim);cursor:pointer;z-index:1;line-height:0;transition:color .15s,background .15s}.copy-btn:hover{background:var(--bg-elev-2);color:var(--fg)}.copy-btn--copied{color:#4ade80;border-color:#4ade80}@media (hover: hover){.copy-btn{opacity:0;pointer-events:none;transition:opacity .15s,color .15s,background .15s}.copy-btn-wrap:hover .copy-btn,.copy-btn-wrap:focus-within .copy-btn{opacity:1;pointer-events:auto}}.copy-btn--code{top:8px;right:8px}.activity-heartbeat{display:flex;align-items:center;gap:6px;padding:4px 8px;margin:0 0 6px;border-radius:6px;border:1px solid rgba(59,130,246,.35);background:var(--bg-elev-2);font-size:12px;color:var(--fg);transition:opacity .3s}.activity-heartbeat.stale{opacity:.5;border-color:var(--border);color:var(--fg-dim)}.heartbeat-spinner{font-family:ui-monospace,monospace;font-size:11px;color:var(--accent);display:inline-block;animation:spin 1.4s linear infinite}.activity-heartbeat.stale .heartbeat-spinner{animation:none;color:var(--fg-dim)}.heartbeat-agent{font-weight:600}.heartbeat-sep{color:var(--fg-dim)}.heartbeat-action{color:var(--fg-dim);flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.heartbeat-elapsed{font-family:ui-monospace,monospace;font-size:11px;color:var(--fg-dim);white-space:nowrap}.activity-strip{margin:0 0 8px;font-size:12px}.activity-summary{display:flex;flex-wrap:wrap;gap:6px}.activity-pill{display:inline-flex;align-items:center;gap:6px;background:var(--bg-elev);border:1px solid var(--border);color:var(--fg-dim);padding:3px 10px;border-radius:999px;cursor:pointer;font-size:12px}.activity-pill:hover{background:var(--bg-elev-2)}.activity-pill.running{color:var(--accent);border-color:#3b82f673}.activity-pill .glyph{font-family:ui-monospace,monospace;font-size:11px}.activity-pill.running .glyph{display:inline-block;animation:spin 1s linear infinite}.activity-pill .caret{color:var(--fg-dim);font-size:10px}.activity-headlines{display:flex;flex-direction:column;gap:2px;margin-top:6px}.activity-headline{display:inline-flex;align-items:center;gap:6px;padding:2px 4px;color:var(--fg-dim)}.activity-headline.status-running{color:var(--accent)}.activity-headline.status-failed{color:var(--danger)}.activity-headline .glyph{font-family:ui-monospace,monospace;font-size:11px;width:12px;text-align:center}.activity-headline.status-running .glyph{animation:spin 1s linear infinite}.agent-tag{font-size:10px;text-transform:lowercase;background:#3b82f629;color:#93c5fd;border:1px solid rgba(59,130,246,.35);padding:1px 6px;border-radius:4px;letter-spacing:.02em}.activity-thinking,.activity-details{margin-top:8px;padding:10px 12px;background:var(--bg-elev);border:1px solid var(--border);border-radius:6px}.activity-details{display:flex;flex-direction:column;gap:6px}.thinking-block{margin:0;padding:8px;background:var(--bg-elev-2);border-radius:4px;white-space:pre-wrap;font-size:12px;line-height:1.5;max-height:280px;overflow:auto}.activity-row{border:1px solid var(--border);border-radius:6px;background:var(--bg-elev-2)}.activity-row.status-running{border-color:#3b82f673}.activity-row.status-failed{border-color:var(--danger)}.activity-row-head{width:100%;display:flex;align-items:center;gap:8px;background:transparent;border:0;color:var(--fg);text-align:left;padding:6px 10px;cursor:pointer;font-size:12px}.activity-row.status-running .activity-row-head .glyph{animation:spin 1s linear infinite;color:var(--accent)}.activity-row.status-failed .activity-row-head .glyph{color:var(--danger)}.activity-row .glyph{font-family:ui-monospace,monospace;width:12px;text-align:center}.activity-row .caret{margin-left:auto;color:var(--fg-dim)}.activity-row-body{padding:0 10px 10px;display:flex;flex-direction:column;gap:6px}.row-label{font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--fg-dim)}.composer{border-top:1px solid var(--border);background:var(--bg-elev);padding:14px 32px;display:flex;flex-direction:column;gap:8px}.composer textarea{width:100%;background:var(--bg);border:1px solid var(--border);border-radius:8px;color:var(--fg);padding:10px;resize:vertical}.composer-help{margin-top:-2px}.dreaming-indicator{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--fg-dim);padding:4px 0;animation:pulse 2s ease-in-out infinite}.dreaming-indicator-glyph{color:#c4b5fd}.composer-actions{display:flex;justify-content:flex-end;gap:6px}.md{line-height:1.55}.md p:first-child{margin-top:0}.md p:last-child{margin-bottom:0}.md pre{background:var(--bg-elev-2);border-radius:6px;padding:12px;overflow:auto}.md pre code{background:transparent;padding:0}.md table{border-collapse:collapse;margin:1em 0}.md th,.md td{border:1px solid var(--border);padding:6px 10px}.workers-layout{display:grid;grid-template-columns:320px 1fr;gap:18px;align-items:start}.workers-list{display:flex;flex-direction:column;gap:6px}.worker-row{text-align:left;background:var(--bg-elev);border:1px solid var(--border);border-radius:8px;padding:10px 12px;cursor:pointer;color:var(--fg)}.worker-row.selected,.worker-row:hover{background:var(--bg-elev-2)}.worker-row-head{display:flex;justify-content:space-between;align-items:center}.worker-status{font-size:11px;font-weight:600;padding:2px 7px;border-radius:10px;text-transform:uppercase;letter-spacing:.04em}.worker-status--running{background:color-mix(in srgb,var(--accent) 15%,transparent);color:var(--accent)}.worker-status--completed{background:color-mix(in srgb,#4caf50 15%,transparent);color:#4caf50}.worker-status--error{background:color-mix(in srgb,#f44336 15%,transparent);color:#f44336}.worker-row-desc{margin-top:4px;font-size:13px;color:var(--fg)}.workers-detail{background:var(--bg-elev);border:1px solid var(--border);border-radius:8px;padding:18px}.worker-detail-description{margin:4px 0 8px;font-size:15px}.worker-detail-slug,.worker-detail-taskid{font-size:.75em;font-family:var(--font-mono, monospace)}.worker-detail-meta{display:flex;flex-wrap:wrap;align-items:center;gap:4px;margin-bottom:8px}.worker-events{display:flex;flex-direction:column;gap:4px;max-height:320px;overflow-y:auto;background:var(--bg-elev-2);border-radius:6px;padding:10px 12px;margin-bottom:12px;font-size:12px;font-family:var(--font-mono, monospace)}.worker-event{display:flex;gap:8px;align-items:baseline;line-height:1.5}.worker-event-ts{color:var(--text-dim, #888);flex-shrink:0;font-size:11px}.worker-event-body{display:flex;gap:4px;align-items:baseline;flex-wrap:wrap;overflow:hidden}.worker-event-icon{flex-shrink:0}.worker-event--tool_complete .worker-event-icon{opacity:.7}.msg-queued-indicator{font-size:.8em;opacity:.6;vertical-align:middle;-webkit-user-select:none;user-select:none}.msg-queued-backend-indicator{display:inline-block;font-size:.78em;opacity:.75;margin-top:4px;color:var(--fg-dim);-webkit-user-select:none;user-select:none}.output{background:var(--bg-elev-2);padding:12px;border-radius:6px;overflow:auto;white-space:pre-wrap;font-size:13px}.projects-toolbar{margin-bottom:16px}.projects-register-form{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.projects-path-input{background:var(--bg-elev);border:1px solid var(--border);border-radius:6px;color:var(--fg);font-size:13px;padding:6px 10px;width:380px;max-width:100%}.projects-path-input:focus{outline:none;border-color:var(--accent)}.projects-register-error{font-size:12px}.projects-disabled{background:var(--bg-elev);border:1px solid var(--border);border-radius:8px;padding:18px}.projects-empty{padding:24px 0}.projects-list{display:flex;flex-direction:column;gap:8px}.project-row{background:var(--bg-elev);border:1px solid var(--border);border-radius:8px;padding:12px 16px;display:flex;align-items:center;justify-content:space-between;gap:12px}.project-row-info{display:flex;flex-direction:column;gap:4px;min-width:0}.project-root{font-size:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.project-meta{display:flex;gap:12px;font-size:12px;flex-wrap:wrap}.project-badge{background:var(--bg-elev-2);border:1px solid var(--border);border-radius:10px;padding:1px 8px;font-size:11px;color:var(--fg)}.project-row-actions{display:flex;gap:6px;flex-shrink:0}.project-chat-header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:8px 16px;background:color-mix(in srgb,var(--accent) 8%,var(--bg-elev));border-bottom:1px solid color-mix(in srgb,var(--accent) 20%,var(--border));flex-shrink:0}.project-chat-header-identity{display:flex;align-items:center;gap:8px;min-width:0;overflow:hidden}.project-chat-icon{font-size:16px;flex-shrink:0}.project-chat-title{font-size:14px;white-space:nowrap;flex-shrink:0}.project-chat-path{font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.wiki{display:flex;flex-direction:column;min-height:100%}.wiki-layout{display:grid;grid-template-columns:minmax(320px,360px) minmax(0,1fr);gap:20px;flex:1;min-height:0}.wiki-sidebar,.wiki-main{min-height:0}.wiki-sidebar{display:flex;flex-direction:column;background:var(--bg-elev);border:1px solid var(--border);border-radius:12px;overflow:hidden}.wiki-sidebar-header{position:sticky;top:0;z-index:1;display:flex;flex-direction:column;gap:14px;padding:16px;border-bottom:1px solid var(--border);background:linear-gradient(180deg,var(--bg-elev) 0%,rgba(22,27,34,.98) 100%)}.wiki-sidebar-header-row{display:flex;justify-content:space-between;align-items:flex-start;gap:12px}.wiki-sidebar-header-row h2{margin:0 0 4px;font-size:16px}.wiki-sidebar-header-row p{margin:0}.wiki-search{display:flex;flex-direction:column;gap:12px}.wiki-search-field input,.wiki-filter select{width:100%;background:var(--bg);border:1px solid var(--border);color:var(--fg);padding:9px 10px;border-radius:8px}.wiki-filter{display:flex;flex-direction:column;gap:6px;font-size:12px;color:var(--fg-dim)}.wiki-search-meta,.wiki-shortcuts,.wiki-scope-legend{color:var(--fg-dim)}.wiki-scope-header-row{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.wiki-scope-header-row h1{margin:0}.wiki-shortcuts{border-top:1px solid var(--border);padding-top:12px}.wiki-scope-legend{display:flex;flex-wrap:wrap;gap:8px}.wiki-scope-legend>span{display:inline-flex;align-items:center;gap:4px}.wiki-sidebar-body{flex:1;min-height:0;overflow:auto;padding:12px}.wiki-tree,.wiki-tree-children{list-style:none;margin:0;padding:0}.wiki-tree-children{margin-top:4px}.wiki-node{margin:2px 0}.wiki-node-button{width:100%;display:flex;align-items:center;gap:8px;padding:7px 10px;background:transparent;border:1px solid transparent;border-radius:8px;color:var(--fg);text-align:left;cursor:pointer}.wiki-node-folder-button{color:var(--fg-dim)}.wiki-node-folder-button:hover,.wiki-node-folder-button.expanded,.wiki-node-page-button:hover{background:var(--bg-elev-2);border-color:var(--border);color:var(--fg)}.wiki-node-page-button{align-items:flex-start}.wiki-node-page-button.selected{background:#3b82f61f;border-color:#3b82f659;box-shadow:inset 2px 0 0 var(--accent)}.wiki-node-icon{width:14px;flex:none;text-align:center;color:var(--fg-dim)}.wiki-node-page-button.selected .wiki-node-icon{color:#93c5fd}.wiki-node-page-button.selected .wiki-node-scope-icon-personal{color:#ddd6fe}.wiki-node-page-button.selected .wiki-node-scope-icon-team{color:#a7f3d0}.wiki-node-content{min-width:0;display:flex;flex:1;flex-direction:column;gap:4px}.wiki-node-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wiki-node-meta{display:flex;flex-wrap:wrap;gap:6px;font-size:11px}.wiki-node-count{margin-left:auto;border:1px solid var(--border);border-radius:999px;padding:0 6px;font-size:11px;color:var(--fg-dim)}.wiki-main{min-width:0;display:flex;background:var(--bg-elev);border:1px solid var(--border);border-radius:12px;overflow:hidden}.wiki-main>.wiki-empty-state{width:100%}.wiki-document{width:100%;min-height:0;display:flex;flex-direction:column}.wiki-page-header{position:sticky;top:0;z-index:1;padding:18px 22px 16px;border-bottom:1px solid var(--border);background:linear-gradient(180deg,var(--bg-elev) 0%,rgba(22,27,34,.98) 100%)}.wiki-page-header-main{display:flex;justify-content:space-between;align-items:flex-start;gap:16px}.wiki-page-title-block h2{margin:0;font-size:28px;line-height:1.2}.wiki-page-summary{margin:8px 0 0;max-width:72ch;color:var(--fg-dim)}.wiki-page-actions{display:flex;gap:8px;flex:none}.wiki-breadcrumbs ol{display:flex;flex-wrap:wrap;gap:8px;list-style:none;margin:0 0 12px;padding:0}.wiki-breadcrumbs li{display:flex;align-items:center}.wiki-breadcrumbs li+li:before{content:"/";margin-right:8px;color:var(--fg-dim)}.wiki-breadcrumb-button{padding:0;border:0;background:transparent;color:var(--fg-dim);cursor:pointer}.wiki-breadcrumb-button:hover{color:var(--fg);text-decoration:underline}.wiki-meta{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:14px;font-size:12px;color:var(--fg-dim)}.wiki-badge,.wiki-tag,.wiki-meta-item{display:inline-flex;align-items:center;border:1px solid var(--border);border-radius:999px;padding:3px 8px;background:var(--bg-elev-2)}.wiki-badge{color:#93c5fd;border-color:#3b82f659}.wiki-scope-badge{display:inline-flex;align-items:center;gap:4px}.wiki-scope-badge-personal{color:#c4b5fd;border-color:#c4b5fd59;background:#c4b5fd14}.wiki-scope-badge-team{color:#6ee7b7;border-color:#6ee7b759;background:#6ee7b714}.wiki-node-scope-icon-personal{color:#c4b5fd}.wiki-node-scope-icon-team{color:#6ee7b7}.wiki-tag{color:var(--fg)}.wiki-meta-path{max-width:100%;overflow:auto;white-space:nowrap}.wiki-document-body{flex:1;min-height:0;overflow:auto}.wiki-article{max-width:76ch;padding:24px 22px 32px}.wiki-empty-state{display:flex;flex-direction:column;align-items:flex-start;justify-content:center;gap:12px;margin:auto;max-width:56ch;padding:32px}.wiki-empty-state.compact{margin:0;max-width:none;padding:20px 12px}.wiki-empty-state h2{margin:0;font-size:20px}.wiki-empty-state p{margin:0;color:var(--fg-dim)}.wiki-empty-state-actions{display:flex;flex-wrap:wrap;gap:8px}@media (max-width: 960px){.wiki-layout{grid-template-columns:1fr}.wiki-sidebar{max-height:50vh}.wiki-page-header-main,.wiki-sidebar-header-row,.wiki-scope-legend{flex-direction:column}.wiki-page-actions{width:100%}.wiki-page-actions .btn{flex:1}}.wiki-edit .row{display:flex;gap:12px;margin-bottom:12px}.wiki-edit input[type=text]{flex:1;background:var(--bg);border:1px solid var(--border);color:var(--fg);padding:8px;border-radius:6px}.wiki-edit label{display:block;width:100%;font-size:12px;color:var(--fg-dim)}.wiki-editor{margin-bottom:16px}.skill-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:14px}.skill-card{background:var(--bg-elev);border:1px solid var(--border);border-radius:8px;padding:14px}.skill-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:4px}.tag{font-size:10px;text-transform:uppercase;padding:2px 6px;border-radius:4px;letter-spacing:.05em;background:var(--bg-elev-2);color:var(--fg-dim)}.tag-bundled{color:#93c5fd}.tag-local{color:#86efac}.tag-global{color:#fcd34d}.settings section{margin-bottom:28px}.settings-field{display:flex;flex-direction:column;gap:6px}.settings-field-label{font-size:12px;color:var(--fg-dim)}.settings select{background:var(--bg);color:var(--fg);border:1px solid var(--border);border-radius:6px;padding:6px 10px}.row{display:flex;align-items:center;gap:8px}.settings-row{align-items:flex-end}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes pulse{0%,to{opacity:.4}50%{opacity:1}}.ralph{max-width:760px}.ralph-section{margin-top:24px}.ralph-section h2{font-size:15px;font-weight:600;margin-bottom:10px;color:var(--fg)}.ralph-status-card{background:var(--bg-elev);border:1px solid var(--border);border-radius:8px;padding:14px 16px;display:flex;flex-direction:column;gap:8px}.ralph-status-row{display:flex;align-items:center;gap:10px;font-size:14px}.ralph-status-label{min-width:90px;color:var(--text-dim, #888);font-size:12px;text-transform:uppercase;letter-spacing:.04em}.ralph-badge{font-size:11px;font-weight:600;padding:2px 8px;border-radius:10px;text-transform:uppercase;letter-spacing:.04em}.ralph-badge--running{background:color-mix(in srgb,var(--accent) 15%,transparent);color:var(--accent)}.ralph-badge--stopped{background:color-mix(in srgb,#888 15%,transparent);color:#888}.ralph-mono{font-family:var(--font-mono, monospace);font-size:13px}.ralph-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:400px}.ralph-controls{background:var(--bg-elev);border:1px solid var(--border);border-radius:8px;padding:14px 16px;display:flex;flex-direction:column;gap:12px}.ralph-form-row{display:flex;align-items:center;gap:10px;font-size:14px}.ralph-form-row label{min-width:140px;color:var(--text-dim, #888);font-size:12px;text-transform:uppercase;letter-spacing:.04em}.ralph-select,.ralph-input{background:var(--bg-elev-2, var(--bg-elev));border:1px solid var(--border);border-radius:5px;color:var(--fg);font-size:13px;padding:5px 8px;min-width:240px}.ralph-input--narrow{min-width:80px;width:80px}.ralph-select:focus,.ralph-input:focus{outline:2px solid var(--accent);outline-offset:1px}.btn{border:none;border-radius:6px;font-size:13px;font-weight:600;padding:6px 14px;cursor:pointer;transition:opacity .1s;align-self:flex-start}.btn:disabled{opacity:.5;cursor:not-allowed}.btn-primary{background:var(--accent);color:#fff}.btn-primary:hover:not(:disabled){opacity:.85}.btn-danger{background:#f44336;color:#fff}.btn-danger:hover:not(:disabled){opacity:.85}.ralph-queue-count{font-weight:400;color:var(--text-dim, #888);font-size:13px}.ralph-queue-source{margin-bottom:8px}.ralph-queue{display:flex;flex-direction:column;gap:6px}.ralph-issue{display:block;text-decoration:none;color:inherit;background:var(--bg-elev);border:1px solid var(--border);border-radius:8px;padding:10px 12px;transition:background .1s}.ralph-issue:hover{background:var(--bg-elev-2)}.ralph-issue-head{display:flex;align-items:baseline;gap:8px;margin-bottom:4px}.ralph-issue-number{font-family:var(--font-mono, monospace);font-size:12px;color:var(--text-dim, #888);flex-shrink:0}.ralph-issue-title{font-size:14px;font-weight:500}.ralph-issue-meta{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.ralph-labels{display:flex;flex-wrap:wrap;gap:4px}.ralph-label{font-size:11px;padding:1px 6px;border-radius:8px;background:color-mix(in srgb,#888 12%,transparent);color:var(--fg)}.ralph-label--agent{background:color-mix(in srgb,var(--accent) 12%,transparent);color:var(--accent)}.ralph-label--triage{background:color-mix(in srgb,#9c27b0 12%,transparent);color:#9c27b0}.ralph-label--urgent{background:color-mix(in srgb,#f44336 12%,transparent);color:#f44336}.link-btn{background:none;border:none;color:var(--accent);font-size:inherit;cursor:pointer;padding:0;text-decoration:underline}
|