@yeaft/webchat-agent 1.0.345 → 1.0.346
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/local-runtime/server/db/connection.js +12 -2
- package/local-runtime/server/db/session-ui-metadata-db.js +38 -21
- package/local-runtime/server/handlers/agent-conversation.js +23 -3
- package/local-runtime/server/handlers/agent-output.js +6 -1
- package/local-runtime/server/handlers/client-conversation.js +120 -28
- package/local-runtime/server/session-catalog.js +18 -11
- package/local-runtime/server/ws-utils.js +20 -5
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +212 -129
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/package.json +1 -1
- package/yeaft/session-message-quote.js +58 -7
- package/yeaft/work-center/bridge.js +3 -3
- package/yeaft/work-center/controller.js +6 -1
- package/yeaft/work-center/coordinator.js +12 -2
- package/yeaft/work-center/mainline-projection.js +210 -61
- package/yeaft/work-center/projection.js +23 -0
- package/yeaft/work-center/runner.js +17 -7
- package/yeaft/work-center/service.js +3 -0
- package/yeaft/work-center/store.js +14 -5
- package/yeaft/work-center/workflow.js +6 -1
|
@@ -235,6 +235,7 @@ db.exec(`
|
|
|
235
235
|
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
236
236
|
catalog_key TEXT NOT NULL,
|
|
237
237
|
pinned INTEGER NOT NULL DEFAULT 0,
|
|
238
|
+
is_hidden INTEGER NOT NULL DEFAULT 0,
|
|
238
239
|
sort_rank INTEGER,
|
|
239
240
|
updated_at INTEGER NOT NULL,
|
|
240
241
|
PRIMARY KEY (user_id, catalog_key)
|
|
@@ -335,10 +336,18 @@ for (const migration of yeaftMigrations) {
|
|
|
335
336
|
const yeaftProjectMigrations = [
|
|
336
337
|
`ALTER TABLE yeaft_projects ADD COLUMN instruction TEXT NOT NULL DEFAULT ''`,
|
|
337
338
|
];
|
|
339
|
+
|
|
340
|
+
const sessionUiMetadataMigrations = [
|
|
341
|
+
`ALTER TABLE session_ui_metadata ADD COLUMN is_hidden INTEGER NOT NULL DEFAULT 0`,
|
|
342
|
+
];
|
|
338
343
|
for (const migration of yeaftProjectMigrations) {
|
|
339
344
|
try { db.exec(migration); } catch (_) { /* column exists */ }
|
|
340
345
|
}
|
|
341
346
|
|
|
347
|
+
for (const migration of sessionUiMetadataMigrations) {
|
|
348
|
+
try { db.exec(migration); } catch (_) { /* column exists */ }
|
|
349
|
+
}
|
|
350
|
+
|
|
342
351
|
for (const migration of migrations) {
|
|
343
352
|
try {
|
|
344
353
|
db.exec(migration);
|
|
@@ -663,10 +672,11 @@ export const stmts = {
|
|
|
663
672
|
`),
|
|
664
673
|
|
|
665
674
|
upsertSessionUiMetadata: db.prepare(`
|
|
666
|
-
INSERT INTO session_ui_metadata (user_id, catalog_key, pinned, sort_rank, updated_at)
|
|
667
|
-
VALUES (?, ?, ?, ?, ?)
|
|
675
|
+
INSERT INTO session_ui_metadata (user_id, catalog_key, pinned, is_hidden, sort_rank, updated_at)
|
|
676
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
668
677
|
ON CONFLICT(user_id, catalog_key) DO UPDATE SET
|
|
669
678
|
pinned = excluded.pinned,
|
|
679
|
+
is_hidden = excluded.is_hidden,
|
|
670
680
|
sort_rank = excluded.sort_rank,
|
|
671
681
|
updated_at = excluded.updated_at
|
|
672
682
|
`),
|
|
@@ -1,36 +1,56 @@
|
|
|
1
1
|
import { stmts, transaction } from './connection.js';
|
|
2
|
+
import { chatCatalogKey, yeaftCatalogKey } from '../session-catalog.js';
|
|
3
|
+
|
|
4
|
+
function normalizeMetadataUpdate(update) {
|
|
5
|
+
if (!update?.catalogKey) throw new Error('Catalog metadata update requires catalogKey');
|
|
6
|
+
if (!['yeaft', 'claude-code', 'copilot'].includes(update.runtimeProvider)) {
|
|
7
|
+
throw new Error('Unknown Session runtime provider during metadata update');
|
|
8
|
+
}
|
|
9
|
+
return {
|
|
10
|
+
...update,
|
|
11
|
+
pinned: typeof update.pinned === 'boolean' ? update.pinned : null,
|
|
12
|
+
hidden: typeof update.hidden === 'boolean' ? update.hidden : null,
|
|
13
|
+
hasSortRank: Object.prototype.hasOwnProperty.call(update, 'sortRank'),
|
|
14
|
+
sortRank: Number.isFinite(update.sortRank) ? update.sortRank : null,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
2
17
|
|
|
3
18
|
export function applySessionUiMetadataUpdates(userId, updates, now = Date.now()) {
|
|
4
19
|
if (!userId || !Array.isArray(updates) || updates.length === 0) return false;
|
|
5
|
-
for (const
|
|
6
|
-
|
|
20
|
+
for (const rawUpdate of updates) {
|
|
21
|
+
const update = normalizeMetadataUpdate(rawUpdate);
|
|
22
|
+
const existing = stmts.getSessionUiMetadata.get(userId, update.catalogKey);
|
|
23
|
+
const pinned = update.pinned ?? (existing?.pinned === 1);
|
|
24
|
+
const hidden = update.hidden ?? (existing?.is_hidden === 1);
|
|
25
|
+
const sortRank = update.hasSortRank
|
|
26
|
+
? update.sortRank
|
|
27
|
+
: (Number.isFinite(existing?.sort_rank) ? existing.sort_rank : null);
|
|
7
28
|
stmts.upsertSessionUiMetadata.run(
|
|
8
29
|
userId,
|
|
9
30
|
update.catalogKey,
|
|
10
|
-
|
|
11
|
-
|
|
31
|
+
pinned ? 1 : 0,
|
|
32
|
+
hidden ? 1 : 0,
|
|
33
|
+
sortRank,
|
|
12
34
|
now,
|
|
13
35
|
);
|
|
14
36
|
if (update.runtimeProvider === 'yeaft') {
|
|
15
37
|
const result = stmts.setYeaftSessionPinnedForAgent.run(
|
|
16
|
-
|
|
38
|
+
pinned ? 1 : 0,
|
|
17
39
|
now,
|
|
18
40
|
update.sessionId,
|
|
19
41
|
userId,
|
|
20
42
|
update.agentId,
|
|
21
43
|
);
|
|
22
44
|
if (result.changes !== 1) throw new Error('Yeaft Session identity changed during metadata update');
|
|
23
|
-
} else
|
|
45
|
+
} else {
|
|
24
46
|
const result = stmts.updateSessionPinnedForRoute.run(
|
|
25
|
-
|
|
47
|
+
pinned ? 1 : 0,
|
|
26
48
|
now,
|
|
27
49
|
update.sessionId,
|
|
28
50
|
update.agentId,
|
|
29
51
|
userId,
|
|
30
52
|
);
|
|
31
53
|
if (result.changes !== 1) throw new Error('Chat Session identity changed during metadata update');
|
|
32
|
-
} else {
|
|
33
|
-
throw new Error('Unknown Session runtime provider during metadata update');
|
|
34
54
|
}
|
|
35
55
|
}
|
|
36
56
|
return true;
|
|
@@ -42,24 +62,13 @@ function mapRow(row) {
|
|
|
42
62
|
userId: row.user_id,
|
|
43
63
|
catalogKey: row.catalog_key,
|
|
44
64
|
pinned: row.pinned === 1,
|
|
65
|
+
hidden: row.is_hidden === 1,
|
|
45
66
|
sortRank: Number.isFinite(row.sort_rank) ? row.sort_rank : null,
|
|
46
67
|
updatedAt: row.updated_at,
|
|
47
68
|
};
|
|
48
69
|
}
|
|
49
70
|
|
|
50
71
|
export const sessionUiMetadataDb = {
|
|
51
|
-
upsert(userId, catalogKey, { pinned = false, sortRank = null } = {}) {
|
|
52
|
-
if (!userId || !catalogKey) return false;
|
|
53
|
-
stmts.upsertSessionUiMetadata.run(
|
|
54
|
-
userId,
|
|
55
|
-
catalogKey,
|
|
56
|
-
pinned ? 1 : 0,
|
|
57
|
-
Number.isFinite(sortRank) ? sortRank : null,
|
|
58
|
-
Date.now(),
|
|
59
|
-
);
|
|
60
|
-
return true;
|
|
61
|
-
},
|
|
62
|
-
|
|
63
72
|
get(userId, catalogKey) {
|
|
64
73
|
if (!userId || !catalogKey) return null;
|
|
65
74
|
return mapRow(stmts.getSessionUiMetadata.get(userId, catalogKey));
|
|
@@ -79,4 +88,12 @@ export const sessionUiMetadataDb = {
|
|
|
79
88
|
if (!userId || !catalogKey) return false;
|
|
80
89
|
return stmts.deleteSessionUiMetadata.run(userId, catalogKey).changes > 0;
|
|
81
90
|
},
|
|
91
|
+
|
|
92
|
+
deleteForRoute(userId, { runtimeProvider, agentId, sessionId } = {}) {
|
|
93
|
+
if (!userId || !agentId || !sessionId) return false;
|
|
94
|
+
const catalogKey = runtimeProvider === 'yeaft'
|
|
95
|
+
? yeaftCatalogKey(agentId, sessionId)
|
|
96
|
+
: chatCatalogKey(sessionId);
|
|
97
|
+
return this.delete(userId, catalogKey);
|
|
98
|
+
},
|
|
82
99
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sessionDb, messageDb } from '../database.js';
|
|
1
|
+
import { sessionDb, messageDb, sessionUiMetadataDb } from '../database.js';
|
|
2
2
|
import {
|
|
3
3
|
broadcastAgentList, notifyConversationUpdate, forwardToClients
|
|
4
4
|
} from '../ws-utils.js';
|
|
@@ -313,16 +313,36 @@ export async function handleAgentConversation(agentId, agent, msg) {
|
|
|
313
313
|
}
|
|
314
314
|
break;
|
|
315
315
|
|
|
316
|
-
case 'conversation_deleted':
|
|
316
|
+
case 'conversation_deleted': {
|
|
317
|
+
// The event came from an authenticated Agent, but the conversation id
|
|
318
|
+
// alone is not an ownership proof: a stale Agent can still hold an id
|
|
319
|
+
// after the Session was moved. Always preserve the established delete
|
|
320
|
+
// lifecycle (deactivate it), but only clear sidebar metadata when the
|
|
321
|
+
// exact persisted owner-scoped Agent/user route matches.
|
|
322
|
+
const persistedSession = sessionDb.get(msg.conversationId);
|
|
323
|
+
const metadataOwnerId = persistedSession?.user_id || null;
|
|
324
|
+
const canClearMetadata = !!persistedSession
|
|
325
|
+
&& persistedSession.agent_id === agentId
|
|
326
|
+
&& !!agent.ownerId
|
|
327
|
+
&& metadataOwnerId === agent.ownerId;
|
|
328
|
+
|
|
317
329
|
agent.conversations.delete(msg.conversationId);
|
|
318
330
|
try {
|
|
319
331
|
sessionDb.setActive(msg.conversationId, false);
|
|
332
|
+
if (canClearMetadata) {
|
|
333
|
+
sessionUiMetadataDb.deleteForRoute(metadataOwnerId, {
|
|
334
|
+
runtimeProvider: persistedSession.provider || 'claude-code',
|
|
335
|
+
agentId,
|
|
336
|
+
sessionId: msg.conversationId,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
320
339
|
} catch (e) {
|
|
321
|
-
console.error('Failed to update
|
|
340
|
+
console.error('Failed to update deleted conversation metadata:', e.message);
|
|
322
341
|
}
|
|
323
342
|
await notifyConversationUpdate(agentId, msg);
|
|
324
343
|
await broadcastAgentList();
|
|
325
344
|
break;
|
|
345
|
+
}
|
|
326
346
|
|
|
327
347
|
case 'history_sessions_list':
|
|
328
348
|
case 'folders_list':
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'crypto';
|
|
2
|
-
import { messageDb, yeaftProjectDb, yeaftSessionDb } from '../database.js';
|
|
2
|
+
import { messageDb, sessionUiMetadataDb, yeaftProjectDb, yeaftSessionDb } from '../database.js';
|
|
3
3
|
import { transaction } from '../db/connection.js';
|
|
4
4
|
import { broadcastAgentList, broadcastSessionCatalog, forwardToClients, sendToAgent, sendToWebClient } from '../ws-utils.js';
|
|
5
5
|
import { webClients, previewFiles } from '../context.js';
|
|
@@ -95,6 +95,11 @@ function syncYeaftSessionMetadata(agentId, agent, event) {
|
|
|
95
95
|
try {
|
|
96
96
|
yeaftSessionDb.deleteForAgent(ownerId, agentId, sessionId);
|
|
97
97
|
yeaftProjectDb.removeSession(ownerId, agentId, sessionId);
|
|
98
|
+
sessionUiMetadataDb.deleteForRoute(ownerId, {
|
|
99
|
+
runtimeProvider: 'yeaft',
|
|
100
|
+
agentId,
|
|
101
|
+
sessionId,
|
|
102
|
+
});
|
|
98
103
|
} catch (e) {
|
|
99
104
|
console.warn('[Server] Yeaft Session metadata cleanup failed:', e?.message || e);
|
|
100
105
|
}
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
import { agents, pendingFiles, trackUserTurn, webClients } from '../context.js';
|
|
12
12
|
import {
|
|
13
13
|
sendToWebClient, forwardToAgent,
|
|
14
|
-
broadcastAgentList, broadcastSessionCatalog, buildSessionCatalog,
|
|
14
|
+
broadcastAgentList, broadcastSessionCatalog, buildSessionCatalog, buildHiddenSessionCatalog,
|
|
15
15
|
verifyConversationOwnership, verifyAgentOwnership
|
|
16
16
|
} from '../ws-utils.js';
|
|
17
17
|
import { routeSessionPin } from './session-pin-router.js';
|
|
@@ -100,16 +100,38 @@ function persistSessionPin(userId, routeRef, pinned) {
|
|
|
100
100
|
}]);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
function catalogMetadataUpdates(rows) {
|
|
104
|
+
const seen = new Set();
|
|
105
|
+
const updates = [];
|
|
106
|
+
for (const row of rows) {
|
|
107
|
+
const routeRef = row?.routeRef;
|
|
108
|
+
if (!row?.catalogKey || seen.has(row.catalogKey)
|
|
109
|
+
|| !routeRef?.runtimeProvider || !routeRef?.agentId || !routeRef?.sessionId) return null;
|
|
110
|
+
seen.add(row.catalogKey);
|
|
111
|
+
updates.push({
|
|
112
|
+
catalogKey: row.catalogKey,
|
|
113
|
+
runtimeProvider: routeRef.runtimeProvider,
|
|
114
|
+
agentId: routeRef.agentId,
|
|
115
|
+
sessionId: routeRef.sessionId,
|
|
116
|
+
pinned: row.pinned === true,
|
|
117
|
+
hidden: row.hidden === true,
|
|
118
|
+
sortRank: updates.length,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return updates;
|
|
122
|
+
}
|
|
123
|
+
|
|
103
124
|
function catalogOrderUpdates(client, items) {
|
|
104
125
|
if (!client?.userId || !Array.isArray(items) || items.length === 0) return null;
|
|
105
126
|
const canonical = buildSessionCatalog(client.userId, client.role);
|
|
127
|
+
const hidden = buildHiddenSessionCatalog(client.userId, client.role);
|
|
106
128
|
if (canonical.length !== items.length) return null;
|
|
107
129
|
const canonicalByKey = new Map(canonical.map(row => [row.catalogKey, row]));
|
|
108
130
|
if (canonicalByKey.size !== canonical.length) return null;
|
|
109
131
|
|
|
110
132
|
const seen = new Set();
|
|
111
|
-
const
|
|
112
|
-
for (const
|
|
133
|
+
const orderedVisible = [];
|
|
134
|
+
for (const item of items) {
|
|
113
135
|
const row = canonicalByKey.get(item?.catalogKey);
|
|
114
136
|
const routeRef = item?.routeRef;
|
|
115
137
|
if (!row || seen.has(item.catalogKey)
|
|
@@ -117,16 +139,44 @@ function catalogOrderUpdates(client, items) {
|
|
|
117
139
|
|| routeRef?.agentId !== row.routeRef?.agentId
|
|
118
140
|
|| routeRef?.sessionId !== row.routeRef?.sessionId) return null;
|
|
119
141
|
seen.add(item.catalogKey);
|
|
120
|
-
|
|
121
|
-
catalogKey: row.catalogKey,
|
|
122
|
-
runtimeProvider: row.routeRef.runtimeProvider,
|
|
123
|
-
agentId: row.routeRef.agentId,
|
|
124
|
-
sessionId: row.routeRef.sessionId,
|
|
125
|
-
pinned: row.pinned === true,
|
|
126
|
-
sortRank,
|
|
127
|
-
});
|
|
142
|
+
orderedVisible.push(row);
|
|
128
143
|
}
|
|
129
|
-
|
|
144
|
+
if (seen.size !== canonical.length) return null;
|
|
145
|
+
|
|
146
|
+
// Hidden rows are deliberately absent from a drag payload, but their stale
|
|
147
|
+
// ranks must not collide with the newly ordered visible catalog when they
|
|
148
|
+
// are restored. Normalize both sets in the same transaction, preserving the
|
|
149
|
+
// visible order chosen by the client and the current hidden-row order.
|
|
150
|
+
return catalogMetadataUpdates([...orderedVisible, ...hidden]);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function catalogVisibilityUpdates(client, row, hidden) {
|
|
154
|
+
if (!client?.userId || !row?.catalogKey || !row?.routeRef) return null;
|
|
155
|
+
const visible = buildSessionCatalog(client.userId, client.role);
|
|
156
|
+
const hiddenRows = buildHiddenSessionCatalog(client.userId, client.role);
|
|
157
|
+
const visibleByKey = new Map(visible.map(item => [item.catalogKey, item]));
|
|
158
|
+
const hiddenByKey = new Map(hiddenRows.map(item => [item.catalogKey, item]));
|
|
159
|
+
if (visibleByKey.size !== visible.length || hiddenByKey.size !== hiddenRows.length) return null;
|
|
160
|
+
|
|
161
|
+
if (hidden) {
|
|
162
|
+
const visibleRow = visibleByKey.get(row.catalogKey);
|
|
163
|
+
if (!visibleRow || hiddenByKey.has(row.catalogKey)) return null;
|
|
164
|
+
return catalogMetadataUpdates([
|
|
165
|
+
...visible.filter(item => item.catalogKey !== row.catalogKey),
|
|
166
|
+
{ ...visibleRow, hidden: true },
|
|
167
|
+
...hiddenRows,
|
|
168
|
+
]);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const hiddenRow = hiddenByKey.get(row.catalogKey);
|
|
172
|
+
if (!hiddenRow || visibleByKey.has(row.catalogKey)) return null;
|
|
173
|
+
|
|
174
|
+
// The UI adds a restored Session after the visible list. Keep that policy on
|
|
175
|
+
// the server and also normalize any still-hidden rows, so no stale rank can
|
|
176
|
+
// collide with this restored row during a later refresh or restore.
|
|
177
|
+
const restored = { ...hiddenRow, hidden: false };
|
|
178
|
+
const remainingHidden = hiddenRows.filter(item => item.catalogKey !== restored.catalogKey);
|
|
179
|
+
return catalogMetadataUpdates([...visible, restored, ...remainingHidden]);
|
|
130
180
|
}
|
|
131
181
|
|
|
132
182
|
export function groupOnlineYeaftSessions(rows, agentRegistry = agents) {
|
|
@@ -472,6 +522,11 @@ export async function handleClientConversation(clientId, client, msg, checkAgent
|
|
|
472
522
|
|
|
473
523
|
try {
|
|
474
524
|
sessionDb.setActive(msg.conversationId, false);
|
|
525
|
+
sessionUiMetadataDb.deleteForRoute(client.userId, {
|
|
526
|
+
runtimeProvider: persisted?.provider || 'claude-code',
|
|
527
|
+
agentId: deleteAgentId,
|
|
528
|
+
sessionId: msg.conversationId,
|
|
529
|
+
});
|
|
475
530
|
const deleteAgent = agents.get(deleteAgentId);
|
|
476
531
|
deleteAgent?.conversations.delete(msg.conversationId);
|
|
477
532
|
await broadcastAgentList();
|
|
@@ -574,31 +629,67 @@ export async function handleClientConversation(clientId, client, msg, checkAgent
|
|
|
574
629
|
if (!client.userId || !msg.catalogKey || !msg.routeRef?.runtimeProvider) break;
|
|
575
630
|
const { runtimeProvider, agentId, sessionId } = msg.routeRef;
|
|
576
631
|
let expectedCatalogKey = null;
|
|
632
|
+
let sessionRow = null;
|
|
577
633
|
if (runtimeProvider === 'yeaft') {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
634
|
+
sessionRow = agentId && sessionId
|
|
635
|
+
? yeaftSessionDb.getForAgent(client.userId, agentId, sessionId)
|
|
636
|
+
: null;
|
|
637
|
+
if (sessionRow) expectedCatalogKey = yeaftCatalogKey(agentId, sessionId);
|
|
581
638
|
} else if (runtimeProvider === 'claude-code' || runtimeProvider === 'copilot') {
|
|
582
|
-
|
|
639
|
+
sessionRow = sessionId ? sessionDb.get(sessionId) : null;
|
|
583
640
|
if (agentId && sessionId
|
|
584
641
|
&& (CONFIG.skipAuth || verifyConversationOwnership(sessionId, client.userId, client.role))
|
|
585
|
-
&&
|
|
586
|
-
&& (
|
|
642
|
+
&& sessionRow?.agent_id === agentId
|
|
643
|
+
&& (sessionRow.provider || 'claude-code') === runtimeProvider) {
|
|
587
644
|
expectedCatalogKey = chatCatalogKey(sessionId);
|
|
588
645
|
}
|
|
589
646
|
}
|
|
590
647
|
const authorized = expectedCatalogKey === msg.catalogKey;
|
|
648
|
+
let currentMetadata = null;
|
|
649
|
+
if (authorized) {
|
|
650
|
+
try {
|
|
651
|
+
currentMetadata = sessionUiMetadataDb.get(client.userId, expectedCatalogKey);
|
|
652
|
+
} catch (e) {
|
|
653
|
+
console.warn('[Server] Session metadata read failed:', e?.message || e);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
const persistedPinned = runtimeProvider === 'yeaft'
|
|
657
|
+
? (sessionRow?.pinned === true || sessionRow?.isPinned === true)
|
|
658
|
+
: sessionRow?.is_pinned === 1;
|
|
659
|
+
const nextPinned = typeof msg.pinned === 'boolean'
|
|
660
|
+
? msg.pinned
|
|
661
|
+
: (currentMetadata?.pinned ?? !!persistedPinned);
|
|
662
|
+
const hasHidden = typeof msg.hidden === 'boolean';
|
|
663
|
+
const nextHidden = hasHidden
|
|
664
|
+
? msg.hidden
|
|
665
|
+
: currentMetadata?.hidden === true;
|
|
666
|
+
const nextSortRank = Number.isFinite(msg.sortRank)
|
|
667
|
+
? msg.sortRank
|
|
668
|
+
: (currentMetadata?.sortRank ?? null);
|
|
669
|
+
const hasSortRank = Object.prototype.hasOwnProperty.call(msg, 'sortRank');
|
|
591
670
|
let persisted = false;
|
|
592
671
|
if (authorized) {
|
|
593
672
|
try {
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
673
|
+
// Only an explicit hidden mutation changes catalog membership. A
|
|
674
|
+
// pinned-only first update has no metadata row yet, so comparing an
|
|
675
|
+
// implicit undefined state to false would incorrectly treat it as a
|
|
676
|
+
// restore and reject the valid visible route.
|
|
677
|
+
const visibilityChanged = hasHidden && (currentMetadata?.hidden === true) !== nextHidden;
|
|
678
|
+
const updates = visibilityChanged
|
|
679
|
+
? catalogVisibilityUpdates(client, {
|
|
680
|
+
catalogKey: expectedCatalogKey,
|
|
681
|
+
routeRef: { runtimeProvider, agentId, sessionId },
|
|
682
|
+
}, nextHidden)
|
|
683
|
+
: [{
|
|
684
|
+
catalogKey: expectedCatalogKey,
|
|
685
|
+
runtimeProvider,
|
|
686
|
+
agentId,
|
|
687
|
+
sessionId,
|
|
688
|
+
pinned: nextPinned,
|
|
689
|
+
hidden: nextHidden,
|
|
690
|
+
...(hasSortRank ? { sortRank: nextSortRank } : {}),
|
|
691
|
+
}];
|
|
692
|
+
persisted = updates ? sessionUiMetadataDb.applyBatch(client.userId, updates) : false;
|
|
602
693
|
if (persisted) await broadcastSessionCatalog(client.userId);
|
|
603
694
|
} catch (e) {
|
|
604
695
|
console.warn('[Server] Session metadata update failed:', e?.message || e);
|
|
@@ -611,8 +702,9 @@ export async function handleClientConversation(clientId, client, msg, checkAgent
|
|
|
611
702
|
catalogKey: msg.catalogKey,
|
|
612
703
|
routeRef: msg.routeRef,
|
|
613
704
|
...(persisted ? {
|
|
614
|
-
pinned:
|
|
615
|
-
|
|
705
|
+
pinned: nextPinned,
|
|
706
|
+
hidden: nextHidden,
|
|
707
|
+
sortRank: nextSortRank,
|
|
616
708
|
} : { error: 'Permission denied or stale Session route' }),
|
|
617
709
|
});
|
|
618
710
|
break;
|
|
@@ -35,11 +35,24 @@ function hasCompleteSortRanks(rows) {
|
|
|
35
35
|
return ranks.every(Number.isFinite) && new Set(ranks).size === rows.length;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
function catalogSort(left, right, ranked = false) {
|
|
39
|
+
if (left.pinned !== right.pinned) return left.pinned ? -1 : 1;
|
|
40
|
+
if (ranked) {
|
|
41
|
+
const leftRank = Number.isFinite(left.sortRank) ? left.sortRank : Number.MAX_SAFE_INTEGER;
|
|
42
|
+
const rightRank = Number.isFinite(right.sortRank) ? right.sortRank : Number.MAX_SAFE_INTEGER;
|
|
43
|
+
if (leftRank !== rightRank) return leftRank - rightRank;
|
|
44
|
+
}
|
|
45
|
+
const metadataDelta = timestampValue(right.metadataUpdatedAt) - timestampValue(left.metadataUpdatedAt);
|
|
46
|
+
if (metadataDelta !== 0) return metadataDelta;
|
|
47
|
+
return left.catalogKey.localeCompare(right.catalogKey);
|
|
48
|
+
}
|
|
49
|
+
|
|
38
50
|
export function projectSessionCatalog({
|
|
39
51
|
chatSessions = [],
|
|
40
52
|
yeaftSessions = [],
|
|
41
53
|
metadata = [],
|
|
42
54
|
onlineAgentIds = new Set(),
|
|
55
|
+
includeHidden = false,
|
|
43
56
|
} = {}) {
|
|
44
57
|
const metadataByKey = new Map(metadata.map(row => [row.catalogKey, row]));
|
|
45
58
|
const onlineAgents = onlineAgentIds instanceof Set ? onlineAgentIds : new Set(onlineAgentIds);
|
|
@@ -50,6 +63,7 @@ export function projectSessionCatalog({
|
|
|
50
63
|
const catalogKey = chatCatalogKey(session.id);
|
|
51
64
|
const runtimeProvider = normalizeChatRuntimeProvider(session.provider);
|
|
52
65
|
const meta = metadataByKey.get(catalogKey) || {};
|
|
66
|
+
if (!includeHidden && meta.hidden === true) continue;
|
|
53
67
|
rows.push({
|
|
54
68
|
catalogKey,
|
|
55
69
|
runtimeProvider,
|
|
@@ -60,6 +74,7 @@ export function projectSessionCatalog({
|
|
|
60
74
|
agentName: session.agent_name || '',
|
|
61
75
|
availability: onlineAgents.has(session.agent_id) ? 'online' : 'offline',
|
|
62
76
|
pinned: meta.pinned ?? session.is_pinned === 1,
|
|
77
|
+
hidden: meta.hidden === true,
|
|
63
78
|
sortRank: meta.sortRank ?? null,
|
|
64
79
|
createdAt: session.created_at || null,
|
|
65
80
|
metadataUpdatedAt: session.metadata_updated_at ?? session.created_at ?? null,
|
|
@@ -69,6 +84,7 @@ export function projectSessionCatalog({
|
|
|
69
84
|
for (const session of yeaftSessions) {
|
|
70
85
|
const catalogKey = yeaftCatalogKey(session.agentId, session.id);
|
|
71
86
|
const meta = metadataByKey.get(catalogKey) || {};
|
|
87
|
+
if (!includeHidden && meta.hidden === true) continue;
|
|
72
88
|
rows.push({
|
|
73
89
|
catalogKey,
|
|
74
90
|
runtimeProvider: 'yeaft',
|
|
@@ -79,6 +95,7 @@ export function projectSessionCatalog({
|
|
|
79
95
|
agentName: session.agentName || '',
|
|
80
96
|
availability: onlineAgents.has(session.agentId) ? 'online' : 'offline',
|
|
81
97
|
pinned: meta.pinned ?? !!session.pinned,
|
|
98
|
+
hidden: meta.hidden === true,
|
|
82
99
|
sortRank: meta.sortRank ?? null,
|
|
83
100
|
createdAt: session.createdAt || null,
|
|
84
101
|
metadataUpdatedAt: session.metadataUpdatedAt ?? session.createdAt ?? null,
|
|
@@ -86,15 +103,5 @@ export function projectSessionCatalog({
|
|
|
86
103
|
}
|
|
87
104
|
|
|
88
105
|
const ranked = hasCompleteSortRanks(rows);
|
|
89
|
-
return rows.sort((left, right) =>
|
|
90
|
-
if (left.pinned !== right.pinned) return left.pinned ? -1 : 1;
|
|
91
|
-
if (ranked) {
|
|
92
|
-
const leftRank = Number.isFinite(left.sortRank) ? left.sortRank : Number.MAX_SAFE_INTEGER;
|
|
93
|
-
const rightRank = Number.isFinite(right.sortRank) ? right.sortRank : Number.MAX_SAFE_INTEGER;
|
|
94
|
-
if (leftRank !== rightRank) return leftRank - rightRank;
|
|
95
|
-
}
|
|
96
|
-
const metadataDelta = timestampValue(right.metadataUpdatedAt) - timestampValue(left.metadataUpdatedAt);
|
|
97
|
-
if (metadataDelta !== 0) return metadataDelta;
|
|
98
|
-
return left.catalogKey.localeCompare(right.catalogKey);
|
|
99
|
-
});
|
|
106
|
+
return rows.sort((left, right) => catalogSort(left, right, ranked));
|
|
100
107
|
}
|
|
@@ -100,7 +100,7 @@ export function verifyPersistedAgentOwnership(agentId, userId, role = null) {
|
|
|
100
100
|
|| yeaftSessionDb.getByUser(userId).some(row => row.agentId === agentId);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
|
|
103
|
+
function catalogProjectionInputs(userId, role = null) {
|
|
104
104
|
const chatSessions = sessionDb.getActiveByUser(userId).filter((session) => {
|
|
105
105
|
if (CONFIG.skipAuth || session.user_id === userId) return true;
|
|
106
106
|
if (session.user_id != null) return false;
|
|
@@ -108,12 +108,25 @@ export function buildSessionCatalog(userId, role = null) {
|
|
|
108
108
|
// offline. This same predicate protects catalog mutations.
|
|
109
109
|
return verifyPersistedAgentOwnership(session.agent_id, userId, role);
|
|
110
110
|
});
|
|
111
|
-
|
|
111
|
+
const yeaftSessions = yeaftSessionDb.getByUser(userId);
|
|
112
|
+
const metadata = sessionUiMetadataDb.getByUser(userId);
|
|
113
|
+
return {
|
|
112
114
|
chatSessions,
|
|
113
|
-
yeaftSessions
|
|
114
|
-
metadata
|
|
115
|
+
yeaftSessions,
|
|
116
|
+
metadata,
|
|
115
117
|
onlineAgentIds: onlineAgentIdsForUser(userId, role),
|
|
116
|
-
}
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function buildSessionCatalog(userId, role = null) {
|
|
122
|
+
return projectSessionCatalog(catalogProjectionInputs(userId, role));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function buildHiddenSessionCatalog(userId, role = null) {
|
|
126
|
+
return projectSessionCatalog({
|
|
127
|
+
...catalogProjectionInputs(userId, role),
|
|
128
|
+
includeHidden: true,
|
|
129
|
+
}).filter(row => row.hidden === true);
|
|
117
130
|
}
|
|
118
131
|
|
|
119
132
|
// Broadcast the owner-scoped read model after canonical Session state changes.
|
|
@@ -126,6 +139,7 @@ export async function broadcastSessionCatalog(userId) {
|
|
|
126
139
|
await sendToWebClient(client, {
|
|
127
140
|
type: 'session_catalog_snapshot',
|
|
128
141
|
catalog: buildSessionCatalog(userId, client.role),
|
|
142
|
+
hiddenCatalog: buildHiddenSessionCatalog(userId, client.role),
|
|
129
143
|
projects: yeaftProjectDb.list(userId),
|
|
130
144
|
projectsAuthoritative: true,
|
|
131
145
|
});
|
|
@@ -189,6 +203,7 @@ export async function broadcastAgentList() {
|
|
|
189
203
|
await sendToWebClient(client, {
|
|
190
204
|
type: 'session_catalog_snapshot',
|
|
191
205
|
catalog: buildSessionCatalog(client.userId, client.role),
|
|
206
|
+
hiddenCatalog: buildHiddenSessionCatalog(client.userId, client.role),
|
|
192
207
|
projects: yeaftProjectDb.list(client.userId),
|
|
193
208
|
projectsAuthoritative: true,
|
|
194
209
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.0.
|
|
1
|
+
{"version":"1.0.346"}
|