newmark-agent 0.3.6 → 0.3.8
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/conversation-utility-host.bundle.cjs +338 -11
- package/dist/core/agent.d.ts +18 -0
- package/dist/core/agent.js +317 -2
- package/dist/core/agentKernelRunner.js +4 -0
- package/dist/core/config.js +2 -0
- package/dist/core/subagent.d.ts +1 -0
- package/dist/core/subagent.js +15 -3
- package/dist/core/toolPolicy.js +2 -0
- package/dist/tools/index.js +16 -6
- package/dist/tools/nativeTools.js +2 -0
- package/dist/ui/index.html +15 -4
- package/dist/wsl-agent-host.bundle.cjs +338 -11
- package/package.json +1 -1
package/dist/core/agent.js
CHANGED
|
@@ -191,6 +191,8 @@ class Agent {
|
|
|
191
191
|
continuations = [];
|
|
192
192
|
activeConversationId = 'default';
|
|
193
193
|
lastCompression = null;
|
|
194
|
+
compressionCache = [];
|
|
195
|
+
nextCompressionCacheId = 1;
|
|
194
196
|
workspaceConversations = new Map();
|
|
195
197
|
isSubagentRuntime = false;
|
|
196
198
|
subagentName = '';
|
|
@@ -3109,6 +3111,7 @@ class Agent {
|
|
|
3109
3111
|
this.workspaceConversations.set(key, {
|
|
3110
3112
|
chatMessages: [...this.chatMessages],
|
|
3111
3113
|
history: [...this.history],
|
|
3114
|
+
compressionCache: [...this.compressionCache],
|
|
3112
3115
|
plan: this.normalizeConversationPlan(this.conversationPlan),
|
|
3113
3116
|
linkedPlan: this.normalizeLinkedPlan(this.linkedPlan),
|
|
3114
3117
|
subagentState: this.subagents.serialize(),
|
|
@@ -3138,6 +3141,7 @@ class Agent {
|
|
|
3138
3141
|
title,
|
|
3139
3142
|
chatMessages: [...this.chatMessages],
|
|
3140
3143
|
history: [...this.history],
|
|
3144
|
+
compressionCache: [...this.compressionCache],
|
|
3141
3145
|
plan: this.normalizeConversationPlan(this.conversationPlan),
|
|
3142
3146
|
linkedPlan: this.normalizeLinkedPlan(this.linkedPlan),
|
|
3143
3147
|
subagentState: this.subagents.serialize(),
|
|
@@ -3167,6 +3171,8 @@ class Agent {
|
|
|
3167
3171
|
if (!key) {
|
|
3168
3172
|
this.chatMessages = [];
|
|
3169
3173
|
this.history = [];
|
|
3174
|
+
this.compressionCache = [];
|
|
3175
|
+
this.nextCompressionCacheId = 1;
|
|
3170
3176
|
this.conversationPlan = { items: [] };
|
|
3171
3177
|
this.linkedPlan = { markdown: '', revision: 0 };
|
|
3172
3178
|
this.workRuns = [];
|
|
@@ -3183,6 +3189,8 @@ class Agent {
|
|
|
3183
3189
|
const saved = this.workspaceConversations.get(key);
|
|
3184
3190
|
if (saved) {
|
|
3185
3191
|
this.history = [...saved.history];
|
|
3192
|
+
this.compressionCache = saved.compressionCache ? saved.compressionCache.map(entry => ({ ...entry, messages: [...entry.messages] })) : [];
|
|
3193
|
+
this.nextCompressionCacheId = Math.max(1, ...this.compressionCache.map(entry => Number(entry.id.replace(/^ctx-cache-/, '')) || 0)) + 1;
|
|
3186
3194
|
this.chatMessages = this.normalizeConversationChatMessages(saved.chatMessages, this.history);
|
|
3187
3195
|
this.conversationPlan = this.normalizeConversationPlan(saved.plan);
|
|
3188
3196
|
this.linkedPlan = this.normalizeLinkedPlan(saved.linkedPlan);
|
|
@@ -3202,6 +3210,8 @@ class Agent {
|
|
|
3202
3210
|
const stateKey = this.workspaceConversationStateKey();
|
|
3203
3211
|
const persisted = stateKey && stored.conversations ? stored.conversations[stateKey] : null;
|
|
3204
3212
|
this.history = persisted?.history ? [...persisted.history] : [];
|
|
3213
|
+
this.compressionCache = persisted?.compressionCache ? persisted.compressionCache.map(entry => ({ ...entry, messages: [...entry.messages] })) : [];
|
|
3214
|
+
this.nextCompressionCacheId = Math.max(1, ...this.compressionCache.map(entry => Number(entry.id.replace(/^ctx-cache-/, '')) || 0)) + 1;
|
|
3205
3215
|
this.chatMessages = this.normalizeConversationChatMessages(persisted?.chatMessages || [], this.history);
|
|
3206
3216
|
this.conversationPlan = this.normalizeConversationPlan(persisted?.plan);
|
|
3207
3217
|
this.linkedPlan = this.normalizeLinkedPlan(persisted?.linkedPlan);
|
|
@@ -3219,6 +3229,7 @@ class Agent {
|
|
|
3219
3229
|
this.workspaceConversations.set(key, {
|
|
3220
3230
|
chatMessages: [...this.chatMessages],
|
|
3221
3231
|
history: [...this.history],
|
|
3232
|
+
compressionCache: [...this.compressionCache],
|
|
3222
3233
|
plan: this.normalizeConversationPlan(this.conversationPlan),
|
|
3223
3234
|
linkedPlan: this.normalizeLinkedPlan(this.linkedPlan),
|
|
3224
3235
|
subagentState: this.subagents.serialize(),
|
|
@@ -3417,6 +3428,257 @@ class Agent {
|
|
|
3417
3428
|
truncatedActivities: Math.max(0, publicEvents.length - activities.length),
|
|
3418
3429
|
});
|
|
3419
3430
|
}
|
|
3431
|
+
async handleContextCompress(args, signal) {
|
|
3432
|
+
let input = {};
|
|
3433
|
+
try {
|
|
3434
|
+
input = JSON.parse(args || '{}');
|
|
3435
|
+
}
|
|
3436
|
+
catch { }
|
|
3437
|
+
if (this.history.length <= 1)
|
|
3438
|
+
return { ok: false, output: '[context_compress] No context to compress.', error: 'No context to compress.' };
|
|
3439
|
+
const previousKeepLast = this.config.getNum('context', 'keep_recent_messages');
|
|
3440
|
+
const keepRecent = Math.max(2, Math.min(60, Math.floor(Number(input.keep_recent) || previousKeepLast || 10)));
|
|
3441
|
+
const force = Boolean(input.force);
|
|
3442
|
+
try {
|
|
3443
|
+
this.config.set('context', 'keep_recent_messages', keepRecent);
|
|
3444
|
+
const msgs = this.history.map(message => ({ ...message }));
|
|
3445
|
+
const provider = this.engineModel();
|
|
3446
|
+
await this.maybeCompress(msgs, provider, signal, this.activeModelName(), force);
|
|
3447
|
+
if (this.history.length <= 1)
|
|
3448
|
+
return { ok: false, output: '[context_compress] Compression skipped: context unchanged.', error: 'Compression skipped.' };
|
|
3449
|
+
return {
|
|
3450
|
+
ok: true,
|
|
3451
|
+
output: JSON.stringify({
|
|
3452
|
+
ok: true,
|
|
3453
|
+
compressed: true,
|
|
3454
|
+
at: this.lastCompression?.at,
|
|
3455
|
+
originalMessages: this.lastCompression?.originalMessages,
|
|
3456
|
+
compressedMessages: this.lastCompression?.compressedMessages,
|
|
3457
|
+
originalChars: this.lastCompression?.originalChars,
|
|
3458
|
+
compressedChars: this.lastCompression?.compressedChars,
|
|
3459
|
+
estimatedTokens: this.lastCompression?.compressedTokens,
|
|
3460
|
+
summary: this.lastCompression?.summary?.slice(0, 2000),
|
|
3461
|
+
model: this.lastCompression?.model,
|
|
3462
|
+
fallback: this.lastCompression?.fallback,
|
|
3463
|
+
displayHistory: { untouched: true, messageCount: this.chatMessages.length },
|
|
3464
|
+
}, null, 2),
|
|
3465
|
+
metadata: { kind: 'context-compress' },
|
|
3466
|
+
};
|
|
3467
|
+
}
|
|
3468
|
+
finally {
|
|
3469
|
+
this.config.set('context', 'keep_recent_messages', previousKeepLast);
|
|
3470
|
+
}
|
|
3471
|
+
}
|
|
3472
|
+
handleContextHistoryManage(args) {
|
|
3473
|
+
let input = {};
|
|
3474
|
+
try {
|
|
3475
|
+
input = JSON.parse(args || '{}');
|
|
3476
|
+
}
|
|
3477
|
+
catch { }
|
|
3478
|
+
const action = String(input.action || '').trim();
|
|
3479
|
+
if (!action)
|
|
3480
|
+
return { ok: false, output: '[context_history_manage] action is required (list|remove|summarize|restore|search|status).', error: 'action is required.' };
|
|
3481
|
+
if (action === 'list') {
|
|
3482
|
+
const limit = Math.max(5, Math.min(400, Math.floor(Number(input.limit || 200))));
|
|
3483
|
+
const entries = this.history.slice(0, limit).map((message, index) => ({
|
|
3484
|
+
position: index,
|
|
3485
|
+
role: String(message.role || ''),
|
|
3486
|
+
name: String(message.name || ''),
|
|
3487
|
+
chars: typeof message.content === 'string' ? message.content.length : JSON.stringify(message.content || '').length,
|
|
3488
|
+
preview: this.compressionHistoryContent(message.content || '').slice(0, 160),
|
|
3489
|
+
}));
|
|
3490
|
+
return {
|
|
3491
|
+
ok: true,
|
|
3492
|
+
output: JSON.stringify({
|
|
3493
|
+
ok: true,
|
|
3494
|
+
action: 'list',
|
|
3495
|
+
entryCount: this.history.length,
|
|
3496
|
+
listed: entries.length,
|
|
3497
|
+
entries,
|
|
3498
|
+
displayHistory: { untouched: true, messageCount: this.chatMessages.length },
|
|
3499
|
+
}, null, 2),
|
|
3500
|
+
metadata: { kind: 'context-history-list' },
|
|
3501
|
+
};
|
|
3502
|
+
}
|
|
3503
|
+
const protectedZone = this.contextHistoryProtectedZone();
|
|
3504
|
+
if (action === 'remove') {
|
|
3505
|
+
const position = Math.floor(Number(input.position));
|
|
3506
|
+
if (!Number.isFinite(position) || position < 0 || position >= this.history.length) {
|
|
3507
|
+
return { ok: false, output: `[context_history_manage] remove position ${position} out of range (0..${this.history.length - 1}).`, error: 'remove position out of range.' };
|
|
3508
|
+
}
|
|
3509
|
+
if (protectedZone.has(position) && !Boolean(input.dangerous)) {
|
|
3510
|
+
return {
|
|
3511
|
+
ok: false,
|
|
3512
|
+
output: `[context_history_manage] remove position ${position} is protected (recent context tail or the last user message). Pass dangerous: true to override.`,
|
|
3513
|
+
error: 'remove position is in the protected context zone.',
|
|
3514
|
+
};
|
|
3515
|
+
}
|
|
3516
|
+
const removed = this.history.splice(position, 1)[0];
|
|
3517
|
+
this.saveWorkspaceConversationState(true);
|
|
3518
|
+
return {
|
|
3519
|
+
ok: true,
|
|
3520
|
+
output: JSON.stringify({
|
|
3521
|
+
ok: true,
|
|
3522
|
+
action: 'remove',
|
|
3523
|
+
removedPosition: position,
|
|
3524
|
+
removedRole: String(removed?.role || ''),
|
|
3525
|
+
remaining: this.history.length,
|
|
3526
|
+
displayHistory: { untouched: true, messageCount: this.chatMessages.length },
|
|
3527
|
+
}, null, 2),
|
|
3528
|
+
metadata: { kind: 'context-history-remove' },
|
|
3529
|
+
};
|
|
3530
|
+
}
|
|
3531
|
+
if (action === 'summarize') {
|
|
3532
|
+
const from = Math.max(0, Math.floor(Number(input.position || 0)));
|
|
3533
|
+
const toRaw = Math.floor(Number(input.to ?? input.position));
|
|
3534
|
+
const to = Number.isFinite(toRaw) ? Math.min(this.history.length - 1, Math.max(from, toRaw)) : from;
|
|
3535
|
+
if (from >= this.history.length)
|
|
3536
|
+
return { ok: false, output: `[context_history_manage] summarize position ${from} out of range (0..${this.history.length - 1}).`, error: 'summarize position out of range.' };
|
|
3537
|
+
if (to - from < 1)
|
|
3538
|
+
return { ok: false, output: '[context_history_manage] summarize requires at least two entries in range.', error: 'summarize requires a range of at least two entries.' };
|
|
3539
|
+
const protectedHit = this.history.slice(from, to + 1).some((_, index) => protectedZone.has(from + index));
|
|
3540
|
+
if (protectedHit && !Boolean(input.dangerous)) {
|
|
3541
|
+
return {
|
|
3542
|
+
ok: false,
|
|
3543
|
+
output: '[context_history_manage] summarize range includes protected entries (recent context tail or the last user message). Pass dangerous: true to override.',
|
|
3544
|
+
error: 'summarize range overlaps the protected context zone.',
|
|
3545
|
+
};
|
|
3546
|
+
}
|
|
3547
|
+
const segment = this.history.slice(from, to + 1);
|
|
3548
|
+
const chars = segment.reduce((sum, message) => sum + (typeof message.content === 'string' ? message.content.length : JSON.stringify(message.content || '').length), 0);
|
|
3549
|
+
const summary = this.localCompressionSummary(`Workspace: ${this.workspace.current?.path || this.rootPath}\nMode: ${this.modeName()}`, segment.map((message, i) => `#${i + 1} [${String(message.role || 'unknown')}${message.name ? ` ${String(message.name)}` : ''}]\n${this.compressionHistoryContent(message.content || '')}`).join('\n\n').slice(0, 20000), segment.length, chars);
|
|
3550
|
+
const replacement = { role: 'system', content: `[Context History Summary]\n${summary}` };
|
|
3551
|
+
this.history.splice(from, to - from + 1, replacement);
|
|
3552
|
+
this.pushCompressionCacheEntry(`[Context History Summary]\n${summary}`, segment, 'local-summarize', true);
|
|
3553
|
+
this.saveWorkspaceConversationState(true);
|
|
3554
|
+
return {
|
|
3555
|
+
ok: true,
|
|
3556
|
+
output: JSON.stringify({
|
|
3557
|
+
ok: true,
|
|
3558
|
+
action: 'summarize',
|
|
3559
|
+
foldedFrom: from,
|
|
3560
|
+
foldedTo: to,
|
|
3561
|
+
foldedEntries: to - from + 1,
|
|
3562
|
+
foldedChars: chars,
|
|
3563
|
+
remaining: this.history.length,
|
|
3564
|
+
summary: summary.slice(0, 2000),
|
|
3565
|
+
displayHistory: { untouched: true, messageCount: this.chatMessages.length },
|
|
3566
|
+
}, null, 2),
|
|
3567
|
+
metadata: { kind: 'context-history-summarize' },
|
|
3568
|
+
};
|
|
3569
|
+
}
|
|
3570
|
+
if (action === 'restore') {
|
|
3571
|
+
const restoreId = String(input.restore_id || '').trim();
|
|
3572
|
+
const entry = this.compressionCache.find(item => item.id === restoreId);
|
|
3573
|
+
if (!entry)
|
|
3574
|
+
return { ok: false, output: `[context_history_manage] restore unknown restore_id: ${restoreId}.`, error: 'restore_id not found.' };
|
|
3575
|
+
const summaryHeader = entry.summary.startsWith('[Context Compression') ? '[Context Compression' : '[Context History Summary]';
|
|
3576
|
+
const markerIndex = this.history.findIndex(message => String(message.role || '') === 'system' && String(message.content || '').includes(summaryHeader) && String(message.content || '').includes(entry.summary.slice(0, 200)));
|
|
3577
|
+
if (markerIndex < 0) {
|
|
3578
|
+
return { ok: false, output: '[context_history_manage] restore failed: the folded summary is no longer present in context history (already re-folded or removed).', error: 'restore target summary not found in history.' };
|
|
3579
|
+
}
|
|
3580
|
+
this.history.splice(markerIndex, 1, ...entry.messages.map(message => ({ ...message })));
|
|
3581
|
+
this.compressionCache = this.compressionCache.filter(item => item.id !== entry.id);
|
|
3582
|
+
this.saveWorkspaceConversationState(true);
|
|
3583
|
+
return {
|
|
3584
|
+
ok: true,
|
|
3585
|
+
output: JSON.stringify({
|
|
3586
|
+
ok: true,
|
|
3587
|
+
action: 'restore',
|
|
3588
|
+
restoreId: entry.id,
|
|
3589
|
+
restoredEntries: entry.messages.length,
|
|
3590
|
+
restoredChars: entry.foldedChars,
|
|
3591
|
+
cacheRemaining: this.compressionCache.length,
|
|
3592
|
+
displayHistory: { untouched: true, messageCount: this.chatMessages.length },
|
|
3593
|
+
}, null, 2),
|
|
3594
|
+
metadata: { kind: 'context-history-restore' },
|
|
3595
|
+
};
|
|
3596
|
+
}
|
|
3597
|
+
if (action === 'search') {
|
|
3598
|
+
const query = String(input.query || '').trim().toLowerCase();
|
|
3599
|
+
const limit = Math.max(1, Math.min(200, Math.floor(Number(input.limit || 20))));
|
|
3600
|
+
if (!query)
|
|
3601
|
+
return { ok: false, output: '[context_history_manage] search requires query.', error: 'search requires query.' };
|
|
3602
|
+
const matches = [];
|
|
3603
|
+
for (const entry of this.compressionCache) {
|
|
3604
|
+
if (matches.length >= limit)
|
|
3605
|
+
break;
|
|
3606
|
+
const hit = { cacheId: entry.id, at: entry.at, summary: entry.summary.slice(0, 500), matches: [] };
|
|
3607
|
+
if (entry.summary.toLowerCase().includes(query)) {
|
|
3608
|
+
hit.matches.push({ index: -1, snippet: this.snippetAround(entry.summary, query) });
|
|
3609
|
+
}
|
|
3610
|
+
entry.messages.forEach((message, index) => {
|
|
3611
|
+
if (matches.length >= limit || hit.matches.length >= 40)
|
|
3612
|
+
return;
|
|
3613
|
+
const content = this.compressionHistoryContent(message.content || message.reasoning_content || '');
|
|
3614
|
+
if (content.toLowerCase().includes(query)) {
|
|
3615
|
+
hit.matches.push({ index, snippet: this.snippetAround(content, query) });
|
|
3616
|
+
}
|
|
3617
|
+
});
|
|
3618
|
+
if (hit.matches.length)
|
|
3619
|
+
matches.push(hit);
|
|
3620
|
+
}
|
|
3621
|
+
return {
|
|
3622
|
+
ok: true,
|
|
3623
|
+
output: JSON.stringify({
|
|
3624
|
+
ok: true,
|
|
3625
|
+
action: 'search',
|
|
3626
|
+
query: String(input.query || ''),
|
|
3627
|
+
cacheEntries: this.compressionCache.length,
|
|
3628
|
+
matches,
|
|
3629
|
+
displayHistory: { untouched: true, messageCount: this.chatMessages.length },
|
|
3630
|
+
}, null, 2),
|
|
3631
|
+
metadata: { kind: 'context-history-search' },
|
|
3632
|
+
};
|
|
3633
|
+
}
|
|
3634
|
+
if (action === 'status') {
|
|
3635
|
+
const budget = this.compressionBudget(this.history);
|
|
3636
|
+
const estimatedTokens = this.estimateContextTokens(this.history);
|
|
3637
|
+
const maxTokens = this.contextMaxTokens();
|
|
3638
|
+
const protectedStartIndex = this.contextHistoryProtectedStartIndex();
|
|
3639
|
+
const lastUserIndex = this.history.map(message => String(message.role || '')).lastIndexOf('user');
|
|
3640
|
+
return {
|
|
3641
|
+
ok: true,
|
|
3642
|
+
output: JSON.stringify({
|
|
3643
|
+
ok: true,
|
|
3644
|
+
action: 'status',
|
|
3645
|
+
historyLength: this.history.length,
|
|
3646
|
+
chatMessages: this.chatMessages.length,
|
|
3647
|
+
estimatedTokens,
|
|
3648
|
+
maxTokens,
|
|
3649
|
+
triggerTokens: budget.triggerTokens,
|
|
3650
|
+
targetTokens: budget.targetTokens,
|
|
3651
|
+
summaryTokens: budget.summaryTokens,
|
|
3652
|
+
usagePercent: maxTokens > 0 ? Math.round((estimatedTokens / maxTokens) * 1000) / 10 : 0,
|
|
3653
|
+
thresholdReached: budget.triggerTokens > 0 && estimatedTokens >= budget.triggerTokens,
|
|
3654
|
+
keepRecentMessages: this.config.getNum('context', 'keep_recent_messages') || 10,
|
|
3655
|
+
lastCompression: this.lastCompression ? {
|
|
3656
|
+
at: this.lastCompression.at,
|
|
3657
|
+
originalMessages: this.lastCompression.originalMessages,
|
|
3658
|
+
compressedMessages: this.lastCompression.compressedMessages,
|
|
3659
|
+
compressedTokens: this.lastCompression.compressedTokens,
|
|
3660
|
+
model: this.lastCompression.model,
|
|
3661
|
+
fallback: this.lastCompression.fallback,
|
|
3662
|
+
} : null,
|
|
3663
|
+
cache: {
|
|
3664
|
+
entries: this.compressionCache.length,
|
|
3665
|
+
totalFoldedEntries: this.compressionCache.reduce((sum, item) => sum + item.foldedEntries, 0),
|
|
3666
|
+
totalFoldedChars: this.compressionCache.reduce((sum, item) => sum + item.foldedChars, 0),
|
|
3667
|
+
ids: this.compressionCache.map(item => item.id),
|
|
3668
|
+
},
|
|
3669
|
+
protectedZone: {
|
|
3670
|
+
preserveRecentMessages: this.config.getNum('context', 'preserve_recent_messages') || 5,
|
|
3671
|
+
protectedStartIndex,
|
|
3672
|
+
lastUserMessageIndex: lastUserIndex,
|
|
3673
|
+
protectedCount: protectedStartIndex >= 0 ? this.history.length - protectedStartIndex : 0,
|
|
3674
|
+
},
|
|
3675
|
+
displayHistory: { untouched: true, messageCount: this.chatMessages.length },
|
|
3676
|
+
}, null, 2),
|
|
3677
|
+
metadata: { kind: 'context-history-status' },
|
|
3678
|
+
};
|
|
3679
|
+
}
|
|
3680
|
+
return { ok: false, output: `[context_history_manage] Unknown action: ${action}`, error: `Unknown action: ${action}` };
|
|
3681
|
+
}
|
|
3420
3682
|
recordContextCompressionStep() {
|
|
3421
3683
|
const runId = this.currentWorkRunId();
|
|
3422
3684
|
if (!runId)
|
|
@@ -3856,6 +4118,7 @@ class Agent {
|
|
|
3856
4118
|
model: fallbackUsed ? 'model-switch-segmented-with-fallback' : modelName,
|
|
3857
4119
|
fallback: fallbackUsed,
|
|
3858
4120
|
};
|
|
4121
|
+
this.pushCompressionCacheEntry(summary, originalMessages.slice(0, recentStart), 'model-switch', fallbackUsed);
|
|
3859
4122
|
this.persistCompressedHistory(summary, recent.length, candidate);
|
|
3860
4123
|
this.saveWorkspaceConversationState(true);
|
|
3861
4124
|
return { compressed: true, rounds, segments, droppedMessages: 0, estimatedTokens: this.estimateContextTokens(candidate), maxTokens };
|
|
@@ -3892,6 +4155,7 @@ class Agent {
|
|
|
3892
4155
|
model: fallbackUsed ? 'model-switch-segmented-with-fallback' : modelName,
|
|
3893
4156
|
fallback: fallbackUsed || droppedMessages > 0,
|
|
3894
4157
|
};
|
|
4158
|
+
this.pushCompressionCacheEntry(summary, originalMessages.slice(0, recentStart), 'model-switch', fallbackUsed || droppedMessages > 0);
|
|
3895
4159
|
this.persistCompressedHistory(summary, recent.length, candidate);
|
|
3896
4160
|
this.saveWorkspaceConversationState(true);
|
|
3897
4161
|
return { compressed: true, rounds, segments, droppedMessages, estimatedTokens: this.estimateContextTokens(candidate), maxTokens };
|
|
@@ -5496,7 +5760,7 @@ class Agent {
|
|
|
5496
5760
|
if (!sa)
|
|
5497
5761
|
return { ok: false, output: `[Subagent] Not found: ${name}`, error: `Not found: ${name}` };
|
|
5498
5762
|
const transcript = sa.messages.map(m => `[${m.role}] ${m.content}`).join('\n');
|
|
5499
|
-
return this.subagents.toToolResult(sa.id, `get.subagent("${sa.name}")\nStatus: ${sa.status}\nModel: ${sa.model}\nMode: ${sa.agentMode}\n\nResult:\n${sa.result || ''}\n\nConversation:\n${transcript}`, true);
|
|
5763
|
+
return this.subagents.toToolResult(sa.id, `get.subagent("${sa.name}", id="${sa.id}")\nStatus: ${sa.status}\nModel: ${sa.model}\nMode: ${sa.agentMode}\n\nResult:\n${sa.result || ''}\n\nConversation:\n${transcript}`, true);
|
|
5500
5764
|
}
|
|
5501
5765
|
catch {
|
|
5502
5766
|
return { ok: false, output: '[Subagent] Invalid result arguments.', error: 'Invalid result arguments.' };
|
|
@@ -6393,7 +6657,7 @@ class Agent {
|
|
|
6393
6657
|
return;
|
|
6394
6658
|
const total = msgs.reduce((sum, m) => sum + (typeof m.content === 'string' ? m.content.length : JSON.stringify(m.content || '').length), 0);
|
|
6395
6659
|
const budget = this.compressionBudget(msgs);
|
|
6396
|
-
if (budget.estimatedTokens < budget.triggerTokens)
|
|
6660
|
+
if (budget.estimatedTokens < budget.triggerTokens && !force)
|
|
6397
6661
|
return;
|
|
6398
6662
|
if (!force && this.lastCompression && String(msgs[0]?.content || '').includes(this.lastCompression.summary)) {
|
|
6399
6663
|
const baselineChars = Math.max(0, Number(this.lastCompression.compressedChars || 0));
|
|
@@ -6450,6 +6714,7 @@ class Agent {
|
|
|
6450
6714
|
model: compression.model,
|
|
6451
6715
|
fallback: compression.fallback,
|
|
6452
6716
|
};
|
|
6717
|
+
this.pushCompressionCacheEntry(compression.summary, middle, compression.model, compression.fallback);
|
|
6453
6718
|
this.persistCompressedHistory(compression.summary, recent.length, msgs);
|
|
6454
6719
|
}
|
|
6455
6720
|
async buildCompressionSummary(middle, totalChars, budget, provider, signal, compressionModel, currentInstruction = '') {
|
|
@@ -6607,6 +6872,56 @@ class Agent {
|
|
|
6607
6872
|
if (this.isSubagentRuntime)
|
|
6608
6873
|
this.subagentContextPersist?.(this.history.map(message => ({ ...message })), this.lastCompression);
|
|
6609
6874
|
}
|
|
6875
|
+
pushCompressionCacheEntry(summary, messages, model, fallback) {
|
|
6876
|
+
if (!messages.length)
|
|
6877
|
+
return;
|
|
6878
|
+
const foldedChars = messages.reduce((sum, message) => sum + (typeof message.content === 'string'
|
|
6879
|
+
? message.content.length
|
|
6880
|
+
: JSON.stringify(message.content || '').length), 0);
|
|
6881
|
+
this.compressionCache.push({
|
|
6882
|
+
id: `ctx-cache-${this.nextCompressionCacheId}`,
|
|
6883
|
+
at: new Date().toISOString(),
|
|
6884
|
+
summary,
|
|
6885
|
+
messages: messages.map(message => ({ ...message })),
|
|
6886
|
+
foldedEntries: messages.length,
|
|
6887
|
+
foldedChars,
|
|
6888
|
+
model,
|
|
6889
|
+
fallback,
|
|
6890
|
+
});
|
|
6891
|
+
this.nextCompressionCacheId += 1;
|
|
6892
|
+
const maxEntries = Math.max(0, Math.floor(this.config.getNum('context', 'compression_cache_max') || 8));
|
|
6893
|
+
if (this.compressionCache.length > maxEntries) {
|
|
6894
|
+
this.compressionCache = this.compressionCache.slice(this.compressionCache.length - maxEntries);
|
|
6895
|
+
}
|
|
6896
|
+
this.saveWorkspaceConversationState(true);
|
|
6897
|
+
}
|
|
6898
|
+
contextHistoryProtectedStartIndex() {
|
|
6899
|
+
const preserve = Math.max(0, Math.floor(this.config.getNum('context', 'preserve_recent_messages') || 5));
|
|
6900
|
+
const lastUserIndex = this.history.map(message => String(message.role || '')).lastIndexOf('user');
|
|
6901
|
+
const candidates = [];
|
|
6902
|
+
if (preserve > 0 && this.history.length > 0)
|
|
6903
|
+
candidates.push(Math.max(0, this.history.length - preserve));
|
|
6904
|
+
if (lastUserIndex >= 0)
|
|
6905
|
+
candidates.push(lastUserIndex);
|
|
6906
|
+
return candidates.length ? Math.min(...candidates) : -1;
|
|
6907
|
+
}
|
|
6908
|
+
contextHistoryProtectedZone() {
|
|
6909
|
+
const start = this.contextHistoryProtectedStartIndex();
|
|
6910
|
+
const zone = new Set();
|
|
6911
|
+
if (start >= 0)
|
|
6912
|
+
for (let i = start; i < this.history.length; i += 1)
|
|
6913
|
+
zone.add(i);
|
|
6914
|
+
return zone;
|
|
6915
|
+
}
|
|
6916
|
+
snippetAround(content, query, radius = 150) {
|
|
6917
|
+
const text = String(content || '');
|
|
6918
|
+
const index = text.toLowerCase().indexOf(query.toLowerCase());
|
|
6919
|
+
if (index < 0)
|
|
6920
|
+
return text.slice(0, radius * 2);
|
|
6921
|
+
const from = Math.max(0, index - radius);
|
|
6922
|
+
const to = Math.min(text.length, index + query.length + radius);
|
|
6923
|
+
return `${from > 0 ? '…' : ''}${text.slice(from, to).trim()}${to < text.length ? '…' : ''}`;
|
|
6924
|
+
}
|
|
6610
6925
|
buildSystemPrompt() {
|
|
6611
6926
|
const cwd = this.workspace.current?.path || this.rootPath;
|
|
6612
6927
|
const enabledSkills = this.skills.active();
|
|
@@ -1470,6 +1470,10 @@ async function executeNewmarkTool(agent, name, args, inputSchema, signal) {
|
|
|
1470
1470
|
return agent.handleLinkedPlanTool(args);
|
|
1471
1471
|
if (name === 'build_history_query')
|
|
1472
1472
|
return agent.handleBuildHistoryQuery(args);
|
|
1473
|
+
if (name === 'context_compress')
|
|
1474
|
+
return (await agent.handleContextCompress(args, signal)).output;
|
|
1475
|
+
if (name === 'context_history_manage')
|
|
1476
|
+
return agent.handleContextHistoryManage(args).output;
|
|
1473
1477
|
if (name === 'question') {
|
|
1474
1478
|
if (agent.config.getStr('agent', 'option_feedback') === 'fully_autonomous')
|
|
1475
1479
|
return '[question] Disabled by fully_autonomous option feedback.';
|
package/dist/core/config.js
CHANGED
|
@@ -911,6 +911,8 @@ function defaultConfig() {
|
|
|
911
911
|
auto_compress: { _description: "Auto-compress history", _type: "boolean", value: true },
|
|
912
912
|
compress_threshold_chars: { _description: "Compression threshold", _type: "integer", value: 80000 },
|
|
913
913
|
keep_recent_messages: { _description: "Keep recent messages", _type: "integer", value: 10 },
|
|
914
|
+
preserve_recent_messages: { _description: "dev-0.3.8 protected recent-message zone for context_history_manage (0 disables)", _type: "integer", value: 5 },
|
|
915
|
+
compression_cache_max: { _description: "dev-0.3.8 max folded-segment cache entries retained for restore/search", _type: "integer", value: 8 },
|
|
914
916
|
structured_context_v2: { _description: "dev-0.3.0 structured context v2 (orchestrator + fixed order + snapshot)", _type: "boolean", value: true },
|
|
915
917
|
build_history_persistence: { _description: "dev-0.3.0 append-only Build History persistence", _type: "boolean", value: true },
|
|
916
918
|
branch_log_v2: { _description: "dev-0.3.0 branch long-log v2 (epoch summaries)", _type: "boolean", value: true },
|
package/dist/core/subagent.d.ts
CHANGED
package/dist/core/subagent.js
CHANGED
|
@@ -119,6 +119,10 @@ class SubagentManager {
|
|
|
119
119
|
const id = (0, crypto_1.randomUUID)();
|
|
120
120
|
const shortId = id.replace(/-/g, '').slice(0, 8);
|
|
121
121
|
const slug = natureSlug(name);
|
|
122
|
+
// The Agent-facing name is the caller's stable, human-readable label and is
|
|
123
|
+
// deliberately decoupled from the identity. The UUID and its short form are
|
|
124
|
+
// the only identity-bearing fields; the UI renders the short-id-qualified
|
|
125
|
+
// display name while Agent tool interactions accept both name and id.
|
|
122
126
|
const displayName = `${slug}-${shortId}`;
|
|
123
127
|
const qualifiedName = `${displayName}--${id}`;
|
|
124
128
|
const stamp = now();
|
|
@@ -128,7 +132,7 @@ class SubagentManager {
|
|
|
128
132
|
natureSlug: slug,
|
|
129
133
|
displayName,
|
|
130
134
|
qualifiedName,
|
|
131
|
-
name:
|
|
135
|
+
name: slug,
|
|
132
136
|
conversationId: this.conversationId,
|
|
133
137
|
createdByAgentId,
|
|
134
138
|
prompt,
|
|
@@ -139,7 +143,7 @@ class SubagentManager {
|
|
|
139
143
|
flowName: flowName || undefined,
|
|
140
144
|
flowPc: Math.max(0, Math.floor(Number(flowPc) || 0)),
|
|
141
145
|
status: 'queued',
|
|
142
|
-
messages: [{ role: 'system', content: `Peer agent '${
|
|
146
|
+
messages: [{ role: 'system', content: `Peer agent '${slug}' (${id}): ${prompt}` }, { role: 'user', content: prompt, hidden_user_input: true }],
|
|
143
147
|
result: null,
|
|
144
148
|
createdAt: stamp,
|
|
145
149
|
updatedAt: stamp,
|
|
@@ -152,7 +156,14 @@ class SubagentManager {
|
|
|
152
156
|
return id;
|
|
153
157
|
}
|
|
154
158
|
get(id) {
|
|
155
|
-
|
|
159
|
+
if (this.subs.has(id))
|
|
160
|
+
return this.subs.get(id);
|
|
161
|
+
const exact = [...this.subs.values()].find(item => item.id === id || item.qualifiedName === id);
|
|
162
|
+
if (exact)
|
|
163
|
+
return exact;
|
|
164
|
+
// name is now caller-supplied and not identity-bearing, so it is a
|
|
165
|
+
// convenience lookup only; the id/shortId/displayName paths stay exact.
|
|
166
|
+
return [...this.subs.values()].find(item => item.name === id || item.displayName === id || item.shortId === id || item.natureSlug === natureSlug(id));
|
|
156
167
|
}
|
|
157
168
|
send(id, prompt) {
|
|
158
169
|
const target = this.get(id);
|
|
@@ -321,6 +332,7 @@ class SubagentManager {
|
|
|
321
332
|
natureSlug: record.natureSlug,
|
|
322
333
|
displayName: record.displayName,
|
|
323
334
|
qualifiedName: record.qualifiedName,
|
|
335
|
+
name: record.name,
|
|
324
336
|
createdByAgentId: record.createdByAgentId,
|
|
325
337
|
status: record.status,
|
|
326
338
|
active: record.status !== 'closed',
|
package/dist/core/toolPolicy.js
CHANGED
package/dist/tools/index.js
CHANGED
|
@@ -365,14 +365,24 @@ class ToolExecutor {
|
|
|
365
365
|
remote_root: { type: 'string' },
|
|
366
366
|
remote_path: { type: 'string' },
|
|
367
367
|
}, ['action']),
|
|
368
|
-
t('task', 'Create a same-conversation peer agent and return immediately. The peer has a
|
|
369
|
-
t('subagent_list', 'List flat same-conversation peer agents, optionally filtered by status.', { status: { type: 'string', enum: ['idle', 'queued', 'working', 'completed', 'error', 'closed'] } }, []),
|
|
370
|
-
t('subagent_read', 'Read one same-conversation peer status, queue/mailbox summary, latest bounded feedback, and result. Available for running, queued, completed, error, and closed peers.', { id: { type: 'string' }, name: { type: 'string' }, max_chars: { type: 'number', description: 'Bounded result size from 2000 to 32000 characters.' } }, []),
|
|
371
|
-
t('subagent_send', 'Persist a mailbox message to a same-conversation peer agent.', { id: { type: 'string' }, name: { type: 'string' }, message: { type: 'string' }, prompt: { type: 'string', description: 'Legacy alias for message.' }, kind: { type: 'string', enum: ['directive', 'question', 'result', 'handoff'] }, reply_to: { type: 'string' }, correlation_id: { type: 'string' } }, []),
|
|
372
|
-
t('subagent_result', 'Return the persisted transcript, mailbox summary, status, and latest result for a peer agent.', { id: { type: 'string' }, name: { type: 'string' } }, []),
|
|
373
|
-
t('subagent_close', 'Close a same-conversation peer. Root can close any peer; a peer can close only itself.', { id: { type: 'string' }, name: { type: 'string' } }, []),
|
|
368
|
+
t('task', 'Create a same-conversation peer agent and return immediately. The peer has a stable human-readable name, a short id, and a canonical UUID-qualified identity. The peer name is decoupled from its id: pass name for readable references and id for exact targeting. Pass model to select an exact configured model deployment (deployment:providerId:modelId or an unambiguous provider/model name). When model is omitted, the peer inherits the parent Agent\'s currently resolved model deployment. Plan mode peers are forced to Plan.', { nature: { type: 'string' }, name: { type: 'string', description: 'Legacy alias for nature.' }, prompt: { type: 'string' }, preset: { type: 'string' }, agent: { type: 'string' }, model: { type: 'string', description: 'Optional exact model deployment. Omit to inherit the parent Agent resolved model.' }, mode: { type: 'string' }, input_mode: { type: 'string' }, flow: { type: 'string' } }, ['prompt']),
|
|
369
|
+
t('subagent_list', 'List flat same-conversation peer agents, optionally filtered by status. Each entry exposes both the stable name and the exact id; use the id for any subsequent targeting.', { status: { type: 'string', enum: ['idle', 'queued', 'working', 'completed', 'error', 'closed'] } }, []),
|
|
370
|
+
t('subagent_read', 'Read one same-conversation peer status, queue/mailbox summary, latest bounded feedback, and result. Available for running, queued, completed, error, and closed peers. Pass the exact id returned by subagent_list, or a name for convenience lookup.', { id: { type: 'string', description: 'Exact peer id from subagent_list.' }, name: { type: 'string', description: 'Convenience peer name; ambiguous names resolve to the first match.' }, max_chars: { type: 'number', description: 'Bounded result size from 2000 to 32000 characters.' } }, []),
|
|
371
|
+
t('subagent_send', 'Persist a mailbox message to a same-conversation peer agent. Target by exact id (preferred) or name.', { id: { type: 'string', description: 'Exact peer id from subagent_list.' }, name: { type: 'string', description: 'Convenience peer name.' }, message: { type: 'string' }, prompt: { type: 'string', description: 'Legacy alias for message.' }, kind: { type: 'string', enum: ['directive', 'question', 'result', 'handoff'] }, reply_to: { type: 'string' }, correlation_id: { type: 'string' } }, []),
|
|
372
|
+
t('subagent_result', 'Return the persisted transcript, mailbox summary, status, and latest result for a peer agent. Target by exact id (preferred) or name.', { id: { type: 'string', description: 'Exact peer id from subagent_list.' }, name: { type: 'string', description: 'Convenience peer name.' } }, []),
|
|
373
|
+
t('subagent_close', 'Close a same-conversation peer. Root can close any peer; a peer can close only itself. Target by exact id (preferred) or name.', { id: { type: 'string', description: 'Exact peer id from subagent_list.' }, name: { type: 'string', description: 'Convenience peer name.' } }, []),
|
|
374
374
|
t('linked_plan', 'Read or update the current conversation linked Markdown plan. Update requires the current expected_revision.', { action: { type: 'string', enum: ['get', 'update'] }, markdown: { type: 'string' }, expected_revision: { type: 'number' } }, ['action']),
|
|
375
375
|
t('build_history_query', 'Read the concrete public work details of one historical Build Block. The prompt already exposes user input, final summary, and completion status; call this read-only tool only when the user asks what specifically happened. Select by newest-to-oldest history_index, or by run_id returned from an earlier query.', { history_index: { type: 'number', minimum: 1, description: '1-based historical Build Block index from the request ledger; 1 is the newest previous task.' }, run_id: { type: 'string', description: 'Exact run id returned by an earlier build_history_query result.' }, max_events: { type: 'number', minimum: 1, maximum: 200, description: 'Maximum trailing public work events; defaults to 80.' } }, []),
|
|
376
|
+
t('context_compress', 'Actively compress the LLM context history for this conversation. This collapses older history entries into a concise summary while preserving the recent tail, which reduces context tokens and cost. IMPORTANT: it affects only the LLM context (what the model sees); the displayed conversation history shown to the user is never altered. Call this when the conversation is long, token pressure is high, or you judge that older turns are no longer needed in full. Idempotent and safe: repeated calls produce incremental summaries.', { keep_recent: { type: 'number', minimum: 2, maximum: 60, description: 'Recent message count to keep uncompressed at the tail. Defaults to the configured keep_recent_messages.' }, force: { type: 'boolean', description: 'Compress even if the context is not yet over the automatic threshold. Defaults to false.' } }, []),
|
|
377
|
+
t('context_history_manage', 'Manage the LLM context history for this conversation without affecting the displayed conversation history. This is the active context-management surface. Actions: list returns a bounded index of context entries (position, role, name, length, first-line preview); remove deletes the entry at a given position; summarize replaces a contiguous range of context entries with a concise local summary entry; restore reinserts the original messages of a folded segment from the compression cache using restore_id; search finds which cached folded segments contain a query (and the matching lines); status reports usage vs trigger/target budgets, the last compression, the compression cache, and the protected recent-message zone. The displayed conversation history (what the user sees) is never modified by any action. The recent context tail and the last user message are protected from remove/summarize unless dangerous is true.', {
|
|
378
|
+
action: { type: 'string', enum: ['list', 'remove', 'summarize', 'restore', 'search', 'status'], description: 'list: index context entries. remove: delete the entry at position. summarize: fold entries [from, to] into one summary entry. restore: reinsert cached original messages by restore_id. search: find folded cache entries containing query. status: report context usage, budgets, cache, and protected zone.' },
|
|
379
|
+
position: { type: 'number', minimum: 0, description: '0-based context entry index for remove, or the start of the range for summarize.' },
|
|
380
|
+
to: { type: 'number', minimum: 0, description: '0-based inclusive end of the range for summarize. Defaults to position.' },
|
|
381
|
+
limit: { type: 'number', minimum: 5, maximum: 400, description: 'Maximum context entries to list (default 200), or search matches to return (default 20).' },
|
|
382
|
+
restore_id: { type: 'string', description: 'Cache id of a folded segment (from search or status) to restore into context.' },
|
|
383
|
+
query: { type: 'string', description: 'Case-insensitive text to search for across cached folded segments and their summaries.' },
|
|
384
|
+
dangerous: { type: 'boolean', description: 'Set true to override the protected recent-message zone and allow removing/summarizing entries that include the recent context tail or the last user message.' },
|
|
385
|
+
}, ['action']),
|
|
376
386
|
t('question', 'Ask user a multiple-choice question', { questions: { type: 'array' } }, ['questions']),
|
|
377
387
|
t('skill_download', 'Download a skill', { name: { type: 'string' }, source: { type: 'string' } }, ['name', 'source']),
|
|
378
388
|
t('skill', 'Search enabled skill metadata or load one exact skill body on demand. Use query when unsure, then name to load the selected skill.', { query: { type: 'string', maxLength: 200 }, name: { type: 'string', maxLength: 200 } }, []),
|
|
@@ -40,6 +40,8 @@ exports.NATIVE_TOOL_CATALOG = [
|
|
|
40
40
|
{ name: 'subagent_close', label: 'Subagent close', description: 'Close a same-conversation peer agent.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
|
|
41
41
|
{ name: 'linked_plan', label: 'Linked plan', description: 'Read or conservatively update the conversation-linked Markdown plan.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
|
|
42
42
|
{ name: 'build_history_query', label: 'Build history query', description: 'Read concrete public work details for one historical Build Block.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
|
|
43
|
+
{ name: 'context_compress', label: 'Context compress', description: 'Actively compress the LLM context history, leaving the displayed conversation history unchanged.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
|
|
44
|
+
{ name: 'context_history_manage', label: 'Context history manage', description: 'List, remove, or summarize entries in the LLM context history without touching the displayed conversation history.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
|
|
43
45
|
{ name: 'question', label: 'Ask question', description: 'Ask the user for structured option feedback.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
|
|
44
46
|
{ name: 'skill_download', label: 'Skill download', description: 'Download and install a skill.', category: 'agent', defaultEnabled: true },
|
|
45
47
|
{ name: 'skill', label: 'Skill', description: 'Search enabled skill metadata or load one skill body on demand.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
|
package/dist/ui/index.html
CHANGED
|
@@ -2959,6 +2959,10 @@ button:focus, select:focus, input:focus, textarea:focus, [contenteditable]:focus
|
|
|
2959
2959
|
.work-review-btn { height:28px; padding:0 10px; border:1px solid var(--glass-border-2); border-radius:6px; background:transparent; color:var(--text); cursor:pointer; font:600 11px var(--font); }
|
|
2960
2960
|
.work-review-btn:hover { background:var(--control-hover-bg); border-color:var(--border-hover); }
|
|
2961
2961
|
.work-review-list { border-top:1px solid var(--glass-border-1); }
|
|
2962
|
+
.work-review.collapsed .work-review-list { display: none; }
|
|
2963
|
+
.work-review-head { cursor: pointer; }
|
|
2964
|
+
.work-review-chevron { width:8px; height:8px; flex:0 0 auto; border-right:1px solid var(--text-dim); border-bottom:1px solid var(--text-dim); transform:rotate(-45deg); transition:transform 150ms ease; }
|
|
2965
|
+
.work-review.collapsed .work-review-chevron { transform:rotate(45deg); }
|
|
2962
2966
|
.work-review-file { min-height:34px; display:grid; grid-template-columns:minmax(0,1fr) auto; align-items:center; gap:12px; padding:0 12px; color:var(--text); }
|
|
2963
2967
|
.work-review-file:hover { background:var(--review-row-hover); }
|
|
2964
2968
|
.work-review-path { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font:11px var(--font-mono); }
|
|
@@ -8208,6 +8212,12 @@ function normalizeWorkReviewDiffs(diffs) {
|
|
|
8208
8212
|
return Object.keys(byPath).map(function(key) { return byPath[key]; });
|
|
8209
8213
|
}
|
|
8210
8214
|
|
|
8215
|
+
window.toggleWorkReview = function(head) {
|
|
8216
|
+
var review = head && head.closest ? head.closest('.work-review') : null;
|
|
8217
|
+
if (!review) return;
|
|
8218
|
+
review.classList.toggle('collapsed');
|
|
8219
|
+
};
|
|
8220
|
+
|
|
8211
8221
|
window.toggleWorkReviewFiles = function(button) {
|
|
8212
8222
|
var review = button && button.closest ? button.closest('.work-review') : null;
|
|
8213
8223
|
if (!review) return;
|
|
@@ -8301,15 +8311,16 @@ function addWorkReview(diffs) {
|
|
|
8301
8311
|
var added = files.reduce(function(total, file) { return total + file.added; }, 0);
|
|
8302
8312
|
var deleted = files.reduce(function(total, file) { return total + file.deleted; }, 0);
|
|
8303
8313
|
var review = document.createElement('div');
|
|
8304
|
-
review.className = 'work-review';
|
|
8314
|
+
review.className = 'work-review collapsed';
|
|
8305
8315
|
review.setAttribute('data-files', JSON.stringify(files));
|
|
8306
8316
|
var rows = files.map(function(file, index) {
|
|
8307
8317
|
return '<div class="work-review-file"' + (index >= 3 ? ' style="display:none"' : '') + '><span class="work-review-path">' + esc(file.path) + '</span><span><span class="work-review-add">+' + file.added + '</span><span class="work-review-del">-' + file.deleted + '</span></span></div>';
|
|
8308
8318
|
}).join('');
|
|
8309
8319
|
var editedLabel = files.length === 1 ? t('review.editedOne') : t('review.editedMany').replace('{count}', files.length);
|
|
8310
|
-
review.innerHTML = '<div class="work-review-head"><div class="work-review-mark">' + iconSvg('file-diff', t('review.fileChanges'), 'small') + '</div>' +
|
|
8320
|
+
review.innerHTML = '<div class="work-review-head" onclick="window.toggleWorkReview(this)"><div class="work-review-mark">' + iconSvg('file-diff', t('review.fileChanges'), 'small') + '</div>' +
|
|
8311
8321
|
'<div><div class="work-review-title">' + esc(editedLabel) + '</div><div class="work-review-stats"><span class="work-review-add">+' + added + '</span><span class="work-review-del">-' + deleted + '</span></div></div>' +
|
|
8312
|
-
'<div class="work-review-actions"><button class="work-review-btn" onclick="window.openWorkReview(this)">' + esc(t('review.open')) + '</button
|
|
8322
|
+
'<div class="work-review-actions"><button class="work-review-btn" onclick="window.openWorkReview(this);event.stopPropagation()">' + esc(t('review.open')) + '</button>' +
|
|
8323
|
+
'<span class="work-review-chevron" aria-hidden="true"></span></div></div>' +
|
|
8313
8324
|
'<div class="work-review-list">' + rows + (files.length > 3 ? '<button class="work-review-more" onclick="window.toggleWorkReviewFiles(this)">' + esc(t('review.showMore').replace('{count}', files.length - 3)) + '</button>' : '') + '</div>';
|
|
8314
8325
|
els['chat-area'].appendChild(review);
|
|
8315
8326
|
autoScrollIfAtBottom();
|
|
@@ -9112,7 +9123,7 @@ function workToolActivity(event) {
|
|
|
9112
9123
|
if (name === 'task' || name.indexOf('subagent_') === 0) return { key: 'subagents', type: 'tool_subagent' };
|
|
9113
9124
|
if (name === 'skill' || name === 'skill_load' || name === 'skill_read' || name === 'skill_download') return { key: 'skills', type: 'tool_skill' };
|
|
9114
9125
|
if (name === 'mcp' || name.indexOf('mcp__') === 0 || /^mcp[:._-]/.test(name) || /[:._-]mcp[:._-]/.test(name)) return { key: 'mcp', type: 'tool_mcp' };
|
|
9115
|
-
if (name === 'context_compression') return { key: 'context_compression', type: 'tool_context_compression' };
|
|
9126
|
+
if (name === 'context_compression' || name === 'context_compress' || name === 'context_history_manage') return { key: 'context_compression', type: 'tool_context_compression' };
|
|
9116
9127
|
if (name === 'memory_lab_update' || name === 'memory_lab_reindex') return { key: 'memory_lab', type: 'tool_memory_lab' };
|
|
9117
9128
|
if (name === 'image_inspect' || name === 'computer_use') return { key: 'images', type: 'tool_images' };
|
|
9118
9129
|
if (name === 'write' || name === 'edit') return { key: 'files', type: 'tool_files' };
|