@yeaft/webchat-agent 1.0.164 → 1.0.166

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.
@@ -29,7 +29,7 @@ import { loadMcpServers, updateMcpConfig } from '../mcp.js';
29
29
  import { getLlmConfig, updateLlmConfig, getYeaftSettings, updateYeaftSettings, getSearchSettings, updateSearchSettings, fetchTavilyUsage } from '../yeaft/config-api.js';
30
30
  import { discoverLlmModels } from '../llm-model-discovery.js';
31
31
  import { fetchModelsDev } from '../yeaft/llm/models-dev.js';
32
- import { handleYeaftSessionSend, handleYeaftAskUserAnswer, handleYeaftSubAgentPrompt, handleYeaftTaskCancel, handleYeaftModeSwitch, handleYeaftModelSwitch, resetYeaftSession, handleYeaftLoadHistory, handleYeaftLoadMoreHistory, handleYeaftAbortThread, handleYeaftAbortAll, handleYeaftAbortTurn, handleYeaftVpSubscribe, handleYeaftVpCreate, handleYeaftVpUpdate, handleYeaftVpDelete, handleYeaftVpRead, handleYeaftListSessions, handleYeaftCreateSession, handleYeaftRenameSession, handleYeaftUpdateSession, handleYeaftUpdateSessionConfig, handleYeaftArchiveSession, handleYeaftDeleteSession, handleYeaftSessionAddMember, handleYeaftSessionRemoveMember, handleYeaftSessionSetDefaultVp, handleYeaftScanWorkdirSessions, handleYeaftRestoreSession, handleYeaftDreamTrigger, handleYeaftFetchToolStats, handleYeaftFetchDebugHistory, handleYeaftMcpList, handleYeaftMcpAdd, handleYeaftMcpRemove, handleYeaftMcpReload, broadcastLanguageChange, broadcastYeaftSessionSnapshotEager, preloadYeaftSkillSlashCommands } from '../yeaft/web-bridge.js';
32
+ import { handleYeaftSessionSend, handleYeaftAskUserAnswer, handleYeaftSubAgentPrompt, handleYeaftTaskCancel, handleYeaftModeSwitch, handleYeaftModelSwitch, resetYeaftSession, handleYeaftLoadHistory, handleYeaftSearchHistory, handleYeaftLoadHistoryWindow, handleYeaftLoadMoreHistory, handleYeaftAbortThread, handleYeaftAbortAll, handleYeaftAbortTurn, handleYeaftVpSubscribe, handleYeaftVpCreate, handleYeaftVpUpdate, handleYeaftVpDelete, handleYeaftVpRead, handleYeaftListSessions, handleYeaftCreateSession, handleYeaftRenameSession, handleYeaftUpdateSession, handleYeaftUpdateSessionConfig, handleYeaftArchiveSession, handleYeaftDeleteSession, handleYeaftSessionAddMember, handleYeaftSessionRemoveMember, handleYeaftSessionSetDefaultVp, handleYeaftScanWorkdirSessions, handleYeaftRestoreSession, handleYeaftDreamTrigger, handleYeaftFetchToolStats, handleYeaftFetchDebugHistory, handleYeaftMcpList, handleYeaftMcpAdd, handleYeaftMcpRemove, handleYeaftMcpReload, broadcastLanguageChange, broadcastYeaftSessionSnapshotEager, preloadYeaftSkillSlashCommands } from '../yeaft/web-bridge.js';
33
33
  import { startYeaftStatusRefresh, forceRefreshYeaftStatus } from '../yeaft/status-cache.js';
34
34
  import { handleWorkCenterRequest } from '../yeaft/work-center/bridge.js';
35
35
 
@@ -479,6 +479,14 @@ export async function handleMessage(msg) {
479
479
  await handleYeaftLoadHistory(msg);
480
480
  break;
481
481
 
482
+ case 'yeaft_search_history':
483
+ await handleYeaftSearchHistory(msg);
484
+ break;
485
+
486
+ case 'yeaft_load_history_window':
487
+ await handleYeaftLoadHistoryWindow(msg);
488
+ break;
489
+
482
490
  case 'yeaft_load_more_history':
483
491
  case 'unify_load_more_history':
484
492
  await handleYeaftLoadMoreHistory(msg);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.164",
3
+ "version": "1.0.166",
4
4
  "description": "Remote worker agent for Yeaft Web Code Agent — connects the native Yeaft engine, CLI providers, and workbench tools",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -1395,6 +1395,105 @@ export class ConversationStore {
1395
1395
  return Number.isFinite(seq) ? seq : null;
1396
1396
  }
1397
1397
 
1398
+ /**
1399
+ * Search user-visible messages inside one Session. The scan is newest-first
1400
+ * and stops as soon as one page plus a `hasMore` sentinel is found, so a
1401
+ * common recent hit does not materialize the full transcript.
1402
+ *
1403
+ * @param {string} sessionId
1404
+ * @param {string} query
1405
+ * @param {{ limit?: number, beforeSeq?: number|null }} [opts]
1406
+ * @returns {{ results: object[], hasMore: boolean, nextBeforeSeq: number|null }}
1407
+ */
1408
+ searchVisibleBySession(sessionId, query, opts = {}) {
1409
+ const needle = typeof query === 'string' ? query.trim().toLocaleLowerCase() : '';
1410
+ if (!sessionId || needle.length < 2) return { results: [], hasMore: false, nextBeforeSeq: null };
1411
+
1412
+ const limit = Math.min(50, Math.max(1, Number.isFinite(opts.limit) ? Math.floor(opts.limit) : 20));
1413
+ const beforeSeq = Number.isFinite(opts.beforeSeq) ? opts.beforeSeq : Infinity;
1414
+ const results = [];
1415
+ const seen = new Set();
1416
+ let hasMore = false;
1417
+
1418
+ for (const message of this.#iterateSessionRows(sessionId, { beforeSeq, desc: true })) {
1419
+ if (!message || message.sessionId !== sessionId || isHiddenConversationRow(message)) continue;
1420
+ if (message.role !== 'user' && message.role !== 'assistant') continue;
1421
+ if (!message.id || seen.has(message.id)) continue;
1422
+ seen.add(message.id);
1423
+
1424
+ const text = this.#visibleSearchText(message.content);
1425
+ const matchIndex = text.toLocaleLowerCase().indexOf(needle);
1426
+ if (matchIndex < 0) continue;
1427
+ if (results.length >= limit) {
1428
+ hasMore = true;
1429
+ break;
1430
+ }
1431
+
1432
+ const seq = parseSeqFromId(message.id);
1433
+ if (!Number.isFinite(seq)) continue;
1434
+ results.push({
1435
+ messageId: message.id,
1436
+ turnId: message.turnId || message.threadId || message.id,
1437
+ seq,
1438
+ role: message.role,
1439
+ speakerVpId: message.speakerVpId || null,
1440
+ timestamp: message.ts || message.time || null,
1441
+ snippet: this.#searchSnippet(text, matchIndex, needle.length),
1442
+ });
1443
+ }
1444
+
1445
+ return {
1446
+ results,
1447
+ hasMore,
1448
+ nextBeforeSeq: hasMore && results.length > 0 ? results[results.length - 1].seq : null,
1449
+ };
1450
+ }
1451
+
1452
+ /**
1453
+ * Load a bounded visible window around a search hit. This deliberately does
1454
+ * not mutate normal older-history cursors: the web client merges the window
1455
+ * into its cache solely to mount and focus the requested virtual-list item.
1456
+ *
1457
+ * @param {string} sessionId
1458
+ * @param {number} anchorSeq
1459
+ * @param {{ beforeTurns?: number, afterTurns?: number }} [opts]
1460
+ * @returns {{ messages: object[], oldestSeq: number|null, hasMoreBefore: boolean }}
1461
+ */
1462
+ loadVisibleWindowBySession(sessionId, anchorSeq, opts = {}) {
1463
+ if (!sessionId || !Number.isFinite(anchorSeq)) {
1464
+ return { messages: [], oldestSeq: null, hasMoreBefore: false };
1465
+ }
1466
+
1467
+ const beforeTurns = Math.min(10, Math.max(1, Number.isFinite(opts.beforeTurns) ? Math.floor(opts.beforeTurns) : 3));
1468
+ const afterTurns = Math.min(10, Math.max(1, Number.isFinite(opts.afterTurns) ? Math.floor(opts.afterTurns) : 3));
1469
+ const before = this.loadVisibleBySession(sessionId, anchorSeq + 1, beforeTurns + 1);
1470
+ const messages = before.messages.slice();
1471
+ const seen = new Set(messages.map(message => message?.id).filter(Boolean));
1472
+ let followingUserTurns = 0;
1473
+
1474
+ for (const message of this.#iterateSessionRows(sessionId, { afterSeq: anchorSeq, desc: false })) {
1475
+ if (!message || message.sessionId !== sessionId || isHiddenConversationRow(message)) continue;
1476
+ if (message.role !== 'user' && message.role !== 'assistant') continue;
1477
+ if (message.role === 'user') {
1478
+ followingUserTurns += 1;
1479
+ if (followingUserTurns > afterTurns) break;
1480
+ }
1481
+ if (message.id && seen.has(message.id)) continue;
1482
+ const projected = this.#projectVisibleMessage(message);
1483
+ if (!projected) continue;
1484
+ if (projected.id) seen.add(projected.id);
1485
+ messages.push(projected);
1486
+ }
1487
+
1488
+ messages.sort(compareMessagesBySeq);
1489
+ const oldestSeq = messages.length > 0 ? parseSeqFromId(messages[0].id) : null;
1490
+ return {
1491
+ messages,
1492
+ oldestSeq: Number.isFinite(oldestSeq) ? oldestSeq : null,
1493
+ hasMoreBefore: before.hasMore,
1494
+ };
1495
+ }
1496
+
1398
1497
  /**
1399
1498
  * Count hot messages.
1400
1499
  *
@@ -2115,6 +2214,33 @@ export class ConversationStore {
2115
2214
  };
2116
2215
  }
2117
2216
 
2217
+ #visibleSearchText(content) {
2218
+ if (typeof content === 'string') return content.replace(/\s+/g, ' ').trim();
2219
+ if (!Array.isArray(content)) return '';
2220
+ return content
2221
+ .filter(part => part && typeof part === 'object' && part.type === 'text')
2222
+ .map(part => typeof part.text === 'string' ? part.text : '')
2223
+ .join(' ')
2224
+ .replace(/\s+/g, ' ')
2225
+ .trim();
2226
+ }
2227
+
2228
+ #searchSnippet(text, matchIndex, needleLength) {
2229
+ const radius = 90;
2230
+ const start = Math.max(0, matchIndex - radius);
2231
+ const end = Math.min(text.length, matchIndex + needleLength + radius);
2232
+ return `${start > 0 ? '…' : ''}${text.slice(start, end)}${end < text.length ? '…' : ''}`;
2233
+ }
2234
+
2235
+ #projectVisibleMessage(message) {
2236
+ if (!message || (message.role !== 'user' && message.role !== 'assistant')) return null;
2237
+ if (message.role !== 'assistant' || !Array.isArray(message.toolCalls) || message.toolCalls.length === 0) {
2238
+ return message;
2239
+ }
2240
+ const { toolCalls, ...rest } = message;
2241
+ return { ...rest, toolSummaryCount: toolCalls.length };
2242
+ }
2243
+
2118
2244
  #readSegmentRows(conversationDir, opts = {}) {
2119
2245
  return this.#segmentStoreForConversationDir(conversationDir).readAll(opts);
2120
2246
  }
@@ -50,6 +50,7 @@ import {
50
50
  restoreSessionToRegistry,
51
51
  readWorkDirRegistry,
52
52
  migrateRegisteredWorkDirSessions,
53
+ resolveSessionYeaftDir,
53
54
  } from './sessions/session-crud.js';
54
55
  import { openSession, loadSessionMeta } from './sessions/session-store.js';
55
56
  import { loadSessionConfig, resolveSessionConfig, SessionConfigError } from './sessions/session-config.js';
@@ -5872,6 +5873,82 @@ export async function handleYeaftLoadHistory(msg) {
5872
5873
  *
5873
5874
  * @param {object} msg — { sessionId, beforeSeq, turns }
5874
5875
  */
5876
+ export async function handleYeaftSearchHistory(msg) {
5877
+ const sessionId = typeof msg?.sessionId === 'string' ? msg.sessionId.trim() : '';
5878
+ const query = typeof msg?.query === 'string' ? msg.query.trim().slice(0, 500) : '';
5879
+ const requestId = typeof msg?.requestId === 'string' ? msg.requestId : null;
5880
+ const beforeSeq = Number.isFinite(msg?.beforeSeq) ? msg.beforeSeq : null;
5881
+ const limit = Math.min(50, Math.max(1, Number.isFinite(msg?.limit) ? Math.floor(msg.limit) : 20));
5882
+ const response = {
5883
+ type: 'yeaft_history_search_result',
5884
+ requestId,
5885
+ sessionId: sessionId || null,
5886
+ query,
5887
+ results: [],
5888
+ hasMore: false,
5889
+ nextBeforeSeq: null,
5890
+ _requestClientId: msg?._requestClientId || null,
5891
+ };
5892
+
5893
+ if (!sessionId || query.length < 2) {
5894
+ sendToServer(response);
5895
+ return;
5896
+ }
5897
+
5898
+ try {
5899
+ const defaultYeaftDir = ctx.CONFIG?.yeaftDir || DEFAULT_YEAFT_DIR;
5900
+ const storeDir = resolveSessionYeaftDir(defaultYeaftDir, sessionId);
5901
+ const store = new ConversationStore(storeDir);
5902
+ const result = store.searchVisibleBySession(sessionId, query, { limit, beforeSeq });
5903
+ sendToServer({ ...response, ...result });
5904
+ } catch (err) {
5905
+ console.error('[Yeaft] Session history search failed:', err?.message || err);
5906
+ sendToServer({ ...response, error: 'search_failed' });
5907
+ }
5908
+ }
5909
+
5910
+ export async function handleYeaftLoadHistoryWindow(msg) {
5911
+ const sessionId = typeof msg?.sessionId === 'string' ? msg.sessionId.trim() : '';
5912
+ const requestId = typeof msg?.requestId === 'string' ? msg.requestId : null;
5913
+ const anchorSeq = Number(msg?.anchorSeq);
5914
+ const anchorMessageId = typeof msg?.anchorMessageId === 'string' ? msg.anchorMessageId : null;
5915
+ const response = {
5916
+ type: 'yeaft_history_window',
5917
+ requestId,
5918
+ conversationId: ensureYeaftConversationId(),
5919
+ sessionId: sessionId || null,
5920
+ anchorMessageId,
5921
+ anchorSeq: Number.isFinite(anchorSeq) ? anchorSeq : null,
5922
+ messages: [],
5923
+ oldestSeq: null,
5924
+ hasMoreBefore: false,
5925
+ _requestClientId: msg?._requestClientId || null,
5926
+ };
5927
+
5928
+ if (!sessionId || !Number.isFinite(anchorSeq)) {
5929
+ sendToServer({ ...response, error: 'invalid_anchor' });
5930
+ return;
5931
+ }
5932
+
5933
+ try {
5934
+ const defaultYeaftDir = ctx.CONFIG?.yeaftDir || DEFAULT_YEAFT_DIR;
5935
+ const storeDir = resolveSessionYeaftDir(defaultYeaftDir, sessionId);
5936
+ const store = new ConversationStore(storeDir);
5937
+ const window = store.loadVisibleWindowBySession(sessionId, anchorSeq, {
5938
+ beforeTurns: msg?.beforeTurns,
5939
+ afterTurns: msg?.afterTurns,
5940
+ });
5941
+ sendToServer({
5942
+ ...response,
5943
+ ...window,
5944
+ messages: projectVisibleHistoryChunkMessages(window.messages),
5945
+ });
5946
+ } catch (err) {
5947
+ console.error('[Yeaft] Session history anchor load failed:', err?.message || err);
5948
+ sendToServer({ ...response, error: 'window_load_failed' });
5949
+ }
5950
+ }
5951
+
5875
5952
  export async function handleYeaftLoadMoreHistory(msg) {
5876
5953
  const sessionId = (msg && typeof msg.sessionId === 'string' && msg.sessionId) || null;
5877
5954
  const perfTraceId = typeof msg?.perfTraceId === 'string' && msg.perfTraceId.trim() ? msg.perfTraceId.trim() : null;