feat: context-window overhaul, slash commands, footer ring; fix session-switch flash (closes #142, #143)#144
Conversation
…on-switch flash Resolves #142 (context window optimisation) and #143 (session-switch flash). Issue #142 - context window overhaul: - Structure-aware truncation: keep latest tool outputs verbatim, collapse older turns to summary skeletons - Per-file dedup: multiple reads of the same path keep only the latest payload - Rolling-summary fallback when thresholds are crossed - MCP per-server explicit opt-out - Large-file read hint to nudge the model toward grep - New slash commands: /compact [focus], /context, /fork [name] - Footer context ring (#ft-ctx) replacing the legacy status dot, colour ramps green/yellow/orange/red Issue #143 - session-switch flash: - _loadSession defers buffered-event replay via setTimeout(..., 0) - Replay burst wrapped in replayStart / replayEnd envelopes - Webview adds _replaying flag silencing ascroll() during the burst; single final scroll on replayEnd Other: - Minor cleanups in Anthropic / OpenAI clients - errors.js copy tweaks - session-store.js persists / replays rolling-summary nodes - README: bump vsix refs to 0.41.0, add v0.41.0 changelog entry
|
AI 审查在超大 diff 下质量会下降,建议:
|
|
|
||
| ### v0.40.4 — Pending Edits Panel · 待审编辑面板 | ||
|
|
||
| - 中文:①**新增「待审编辑」面板**:参考 GitHub Copilot in VS Code 的体验,输入框上方新增一个浮层,实时列出本会话中 Agent 写入 / patch / 替换的所有文件,每项显示 `+行数 / -行数`,新建/删除/二进制文件带 tag。②**点击文件 → 原生 Diff 编辑器**:点任何一项都会打开 VS Code 原生差异编辑器,左侧为 Agent 写入前的快照(由 `deepcopilot-before:` `TextDocumentContentProvider` 提供),右侧为当前磁盘内容,URI 带时间戳防缓存 + `onDidChange` 主动刷新,保证可以重复点击。③**逺条/批量操作**:悬停行出现 ✓ (保留)与 ✕ (丢弃)按钮,头部提供「全部保留 / 全部丢弃」。丢弃会用快照恢复磁盘;保留仅从面板清除。④**跨轮持久**:`pendingEdits` 现在以 session 为维度维护,即使 Agent 完成本轮对话、运行实例被陆续释放,住这场会话仍可反复点击面板项 查看/保留/丢弃。⑤**轻量行级 diff**:新增 `src/chat/diff-utils.js`,采用 LCS 计算 +N/-M;>10k 行或 >100k 字符的文件自动降级为集合 diff。二进制文件(包含 NUL 字节)仅标记 `binary` 不走 diff,避免乱码计数。⑥**与现有「回滚本轮」联动**:Revert / `revert_last_turn` 会同步清空 pendingEdits 面板,不遗留幽灵项。⑦**CSP / 安全**:Webview CSP 未放宽;content provider 仅返回内存中的快照,不访问任何额外路径。 |
There was a problem hiding this comment.
在更新日志中提到的上下文窗口管理重构和会话切换闪屏修复涉及到对会话状态的管理,需确保在多线程环境下不会出现竞态条件。此外,建议在实现过程中增加异常处理,确保在出现错误时能够优雅地处理,而不是导致整个扩展崩溃。
| .ft-ctx-pop-row span { opacity: .7; } | ||
| .ft-ctx-pop-row b { font-weight: 600; font-variant-numeric: tabular-nums; } | ||
| .ft-ctx-pop-tip { margin-top: 8px; padding-top: 8px; border-top: 1px dashed var(--vscode-widget-border, #3a3a3a); font-size: 11px; opacity: .85; } | ||
| .ft-ctx-pop-tip code { background: var(--vscode-textCodeBlock-background, #1e1e1e); padding: 0 4px; border-radius: 3px; } |
There was a problem hiding this comment.
新增的 .ft-ctx 和 .ft-ctx-pop 样式定义中,虽然没有明显的安全漏洞,但需要注意以下几点:
- 可维护性:建议在样式中添加注释,说明这些样式的用途和使用场景,以便后续维护。
- 性能:使用
position: fixed的元素可能会影响页面性能,尤其是在复杂布局中,需确保不会造成重绘或重排问题。 - 响应式设计:建议检查这些样式在不同屏幕尺寸下的表现,确保用户体验一致。
- 可访问性:确保这些新增加的元素在视觉上对所有用户友好,特别是对色盲用户,建议使用更具对比度的颜色。
| if (ftCtxBtn) ftCtxBtn.addEventListener('click', function(e){ e.stopPropagation(); openCtxPop(); }); | ||
| var ftTokens = document.getElementById("ft-tokens"); | ||
| var ftCost = document.getElementById("ft-cost"); | ||
| var ftCache = document.getElementById("ft-cache"); |
There was a problem hiding this comment.
在 updateCtxRing 函数中,pct、tokens 和 win 参数未进行严格的类型检查,可能导致意外行为。建议使用 Number.isFinite 来确保这些值是有效的数字。此外,_lastCtx 的初始化可以考虑使用 Object.freeze 来防止意外修改。
| } | ||
|
|
||
| /* Auto narrow mode based on width (use webview container width, not full VS Code window) */ | ||
| function checkNarrow(){ |
There was a problem hiding this comment.
在 ascroll 函数中,_replaying 的状态管理需要更清晰的注释,确保其他开发者理解其目的。同时,建议在函数开头添加对 msgs 的空值检查,以防止潜在的空指针异常。
| if (dot) dot.className = "dot" + (busy ? " warn" : ""); | ||
| var pb = document.getElementById("prog"); | ||
| if (pb) pb.classList.toggle("on", busy); | ||
| _renderStatus(); |
There was a problem hiding this comment.
在设置 dot.className 时,建议在前面添加空值检查,避免在 dot 为 null 时引发错误。
| s.apiMessages = sanitized; | ||
| } | ||
|
|
||
| const last = s.messages[s.messages.length - 1]; |
There was a problem hiding this comment.
在处理模型配置和上下文窗口时,建议对 getModel 和 resolveModel 的返回值进行更严格的检查,以防止潜在的空指针异常。此外,虽然捕获了异常但未进行任何记录,建议至少记录错误信息,以便后续调试。可以考虑使用 console.error(_e) 或其他日志记录机制。
|
|
||
| async pin(id) { | ||
| const list = this.all(); | ||
| const s = list.find(x => x.id === id); |
There was a problem hiding this comment.
在 fork 方法中,使用 JSON.parse(JSON.stringify(src)) 进行深拷贝的方式在性能上可能不够高效,尤其是当 src 对象较大时。建议考虑使用更高效的深拷贝方法,例如使用 lodash 的 cloneDeep 方法。此外,clone.id 的生成方式可能会导致 ID 冲突,建议使用更可靠的 ID 生成策略。
| } else if (/ECONN|ENOTFOUND|EAI_AGAIN|ETIMEDOUT|network|fetch failed|terminated/i.test(raw)) { | ||
| title = t('errNetwork'); | ||
| tip = t('errTipNetwork'); | ||
| } else if (/aborted/i.test(raw)) { |
There was a problem hiding this comment.
在这里增加了对 'terminated' 的检测,但没有提供具体的错误处理逻辑。建议在捕获到此错误时,提供更详细的错误信息或处理方式,以便于用户理解问题的根源。此外,建议确保所有可能的错误情况都有相应的处理逻辑,以避免潜在的未处理异常。
| } | ||
| return truncate(text); | ||
| } catch (e) { return `Error: ${e.message}`; } | ||
| } |
There was a problem hiding this comment.
- 安全性: 在返回错误信息时,直接使用了
e.message,这可能会泄露内部错误信息,建议使用通用的错误提示,避免暴露敏感信息。 - 异常处理: 当前的异常处理仅返回了错误信息,未对异常进行详细记录或处理,建议增加日志记录,以便后续排查问题。
- 性能: 对于大文件的处理逻辑中,建议考虑对
fileSize的检查放在文件读取之前,以避免不必要的读取操作。
| </button> | ||
| </div> | ||
| <div class="ft-right"> | ||
| <button class="ft-btn" id="apibt" title="${ui.apiTitle}">🔑</button> |
There was a problem hiding this comment.
在这个 hunk 中,新增了一个按钮元素 ft-ctx,需要注意以下几点:
- 安全性:确保按钮的点击事件处理程序没有引入 XSS 漏洞,特别是如果按钮的内容是动态生成的。
- 可访问性:虽然使用了
aria-label,但建议检查该按钮的功能是否对所有用户(包括使用屏幕阅读器的用户)友好。 - 性能:SVG 图形的使用是合适的,但需要确保其不会影响页面的渲染性能,尤其是在复杂的 Webview 中。
- 代码风格:请确保新添加的代码遵循项目的现有代码风格,包括缩进、命名规范等。
建议在按钮点击事件中添加必要的错误处理,以防止潜在的未处理异常。
There was a problem hiding this comment.
Pull request overview
This PR targets v0.41.0 and addresses two core UX/agent-runtime problems in the DeepCopilot VS Code extension: (1) reducing context-window pressure via smarter compaction/dedup/caching and (2) removing session-switch flash/scroll jitter by batching buffered event replay. It also adds user-facing controls (/compact, /context, /fork) and a footer context-usage ring UI.
Changes:
- Overhauls context management: structure-aware tool-result truncation, repeated-read dedup, proactive/emergency compaction (incl. nuclear fallback), and token-aware pre-compaction before persistence.
- Adds UX features: slash commands and a footer context ring driven by
ctxUsageevents; improves session-switch replay to avoid scroll jitter. - Provider enhancements: Anthropic prompt caching; OpenAI-compatible client retry on transient “terminated” resets; MCP tool injection opt-out.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| src/chat/compact.js | Implements structure-aware truncation, repeated-read dedup, rolling/nuclear compaction utilities. |
| src/chat/provider.js | Adds local slash commands and deferred buffered-event replay with replay envelopes. |
| src/chat/agent-loop.js | Emits ctxUsage, adjusts compaction budgets/ladder, MCP tools opt-out, nuclear fallback. |
| src/chat/session-store.js | Allows apiMessages-only append, adds persist-time pre-compaction, introduces session fork. |
| src/tools/file-read.js | Adds medium-large-file hinting to reduce context burn. |
| src/webview/html.js | Replaces legacy footer dot with context ring button markup. |
| media/chat.js | Implements ring updates/popover and replay scroll suppression. |
| media/chat.css | Adds ring/popover styling; removes legacy dot styling. |
| src/api/anthropic-client.js | Adds prompt caching via cache_control on system/tools. |
| src/api/openai-client.js | Retries once on transient “terminated” connection reset. |
| src/errors.js | Treats “terminated” as a network-class error. |
| package.json | Bumps version and adds deepseekAgent.includeMcpTools setting. |
| README.md | Updates VSIX/version references and adds v0.41.0 changelog entry. |
结论:需修改
| const callId = `synthetic_skill_read_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`; | ||
| messages.push({ | ||
| role: 'assistant', | ||
| hrole: 'assistant', |
| for (const ln of lines) { | ||
| const key = ln.match(/^([^:]+:\d+):/); | ||
| const k = key ? key[1] : ln; | ||
| if (seen.has(k)) continue; |
| if (body.length < 400) return m; // not worth replacing small ones | ||
| replaced++; | ||
| return { | ||
| ...m, | ||
| content: `[deduped — this ${meta.name} of "${meta.key.split('::')[1]}" was re-read later in the conversation; see the later tool result for current contents]`, | ||
| }; |
| if (fileSize > 50 * 1024) { | ||
| const kb = Math.round(fileSize / 1024); | ||
| const hint = `\n\n[hint] this file is ${kb} KB — consider \`spawn_agent\` (agent_type=explore) for analysis, or \`read_file\` with start_line/end_line to read a focused range, to save context.`; | ||
| return truncate(text) + hint; |
| var modelTxt = (modelPicker && modelPicker.dataset.model) || 'unknown'; | ||
| pop.innerHTML = | ||
| '<div class="ft-ctx-pop-h">Context usage</div>' + | ||
| '<div class="ft-ctx-pop-row"><span>Used</span><b>' + tk + 'K / ' + wn + 'K (' + pct + '%)</b></div>' + | ||
| '<div class="ft-ctx-pop-row"><span>Model</span><b>' + modelTxt + '</b></div>' + | ||
| '<div class="ft-ctx-pop-row"><span>Mode</span><b>' + (_curIMode||'agent') + '</b></div>' + |
| var C = 50.265; | ||
| ftCtxRing.setAttribute('stroke-dashoffset', String(C * (1 - p/100))); | ||
| ftCtxRing.setAttribute('stroke', p > 85 ? '#e57373' : (p > 65 ? '#ffb74d' : '#66bb6a')); | ||
| ftCtxPct.textContent = (p|0) + '%'; |
| setTimeout(() => { | ||
| // Guard: the active session may have changed again during the | ||
| // 0-ms delay (rapid clicking). In that case the events are still | ||
| // buffered on `run.events`, so the next _loadSession(id) for this | ||
| // session will replay them; we just no-op here. | ||
| if (run.sessionId !== this._store.sessionId) return; | ||
| this._post({ type: 'replayStart', count: evs.length }); |
| // Issue #142 P3-4: `/context` status report. Breaks down current context | ||
| // usage into system / history / tool-def buckets so the user can decide | ||
| // whether to /compact or /fork. | ||
| async _handleContextCommand() { | ||
| try { | ||
| const { estimateMessagesTokens, estimateTokens } = require('./compact'); | ||
| const sid = this._store.sessionId; | ||
| const run = this._activeRun(); | ||
| const cfg = vscode.workspace.getConfiguration('deepseekAgent'); | ||
| const provider = cfg.get('provider') || 'deepseek'; | ||
| const model = cfg.get('defaultModel') || 'deepseek-v4-pro'; | ||
| const { resolveProvider } = require('../providers'); | ||
| let modelCfg = { contextWindow: 65536 }; | ||
| try { | ||
| const p = resolveProvider(provider); | ||
| modelCfg = p?.models?.find(m => m.id === model) || modelCfg; | ||
| } catch { /* fallback */ } | ||
| const window = modelCfg.contextWindow || 65536; | ||
|
|
||
| const msgs = run?.messages || []; | ||
| const historyTok = estimateMessagesTokens(msgs); | ||
| // Rough estimate for system prompt — uses the default builder. | ||
| let sysTok = 0; | ||
| try { | ||
| const { buildSystemPrompt } = require('../prompts/system'); | ||
| const sys = buildSystemPrompt({ provider, model }); | ||
| sysTok = estimateTokens(sys); | ||
| } catch { /* skip */ } | ||
|
|
||
| const total = historyTok + sysTok; | ||
| const pct = Math.min(100, Math.round(total / window * 100)); | ||
| const bar = (() => { | ||
| const w = 20; | ||
| const filled = Math.round(pct / 100 * w); | ||
| return '█'.repeat(filled) + '░'.repeat(w - filled); | ||
| })(); | ||
| const lines = [ | ||
| `📊 Context usage — ${pct}%`, | ||
| `[${bar}] ${Math.round(total/1000)}K / ${Math.round(window/1000)}K tokens`, | ||
| ``, | ||
| `• System prompt : ${Math.round(sysTok/1000)}K`, | ||
| `• History : ${Math.round(historyTok/1000)}K (${msgs.length} msgs)`, | ||
| `• Model : ${provider} / ${model}`, |
| const src = list.find(x => x.id === id); | ||
| if (!src) return null; | ||
| const clone = JSON.parse(JSON.stringify(src)); | ||
| clone.id = `s_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`; |
- media/chat.js: escape modelTxt and mode before innerHTML in openCtxPop (fix js/xss-through-dom, js/html-from-text) - src/chat/session-store.js: use crypto.randomBytes for forked session id instead of Math.random (fix js/insecure-randomness; ids are not security-critical but the alert was tripping CI) - src/chat/agent-loop.js: remove useless assignment to preflightTokens after nuclearCompact; next iteration recomputes it - src/chat/compact.js: delete unused TOOL_RESULT_NUKE_HEAD/TAIL constants (nuclearCompact uses inline 800/200 values) - src/chat/provider.js: drop unused sid binding in _handleContextCommand
|
AI 审查在超大 diff 下质量会下降,建议:
|
| } | ||
| var pop = document.createElement('div'); | ||
| pop.className = 'ft-ctx-pop'; | ||
| var pct = _lastCtx.pct|0; |
There was a problem hiding this comment.
在这个 hunk 中,新增的 _esc 函数用于对输入进行转义,以防止 XSS 攻击,这是一个良好的安全实践。然而,建议在使用 innerHTML 时,尽量避免直接插入 HTML 内容,考虑使用 textContent 或其他安全的 DOM 操作方法来避免潜在的安全风险。此外,建议对 modelTxt 和 _curIMode 的来源进行更严格的验证,以确保它们确实是安全的。
| '<div class="ft-ctx-pop-row"><span>Mode</span><b>' + _esc(_curIMode||'agent') + '</b></div>' + | ||
| '<div class="ft-ctx-pop-tip">' + | ||
| '<div><code>/context</code> — detailed token breakdown</div>' + | ||
| '<div><code>/compact [focus]</code> — summarise history</div>' + |
There was a problem hiding this comment.
在这里,使用 _esc 函数对 modelTxt 和 _curIMode 进行了转义,这是必要的安全措施。然而,建议在构建 HTML 内容时,尽量使用更安全的方式,例如使用 createElement 和 appendChild 方法来构建 DOM,而不是直接拼接字符串,这样可以进一步降低 XSS 攻击的风险。
| } | ||
| } | ||
| if (ctxLimitHit) break; // break while loop — do not call the API | ||
| checkAbort(); |
There was a problem hiding this comment.
在这一段代码中,虽然注释说明了 preflightTokens 不会在此之后被重新读取,但没有提供足够的上下文来解释为什么这样做是安全的。建议添加更多的注释,说明在后续迭代中如何重新计算 preflightTokens,以避免潜在的逻辑错误或状态不一致。此外,考虑到可能的异常情况,建议在 checkAbort() 调用之前添加异常处理逻辑,以确保在出现异常时能够安全退出。
| // were never read — removed to satisfy CodeQL js/useless-assignment-to-local. | ||
|
|
||
| function _truncateBody(body, headKeep, tailKeep) { | ||
| const total = body.length; |
There was a problem hiding this comment.
在此代码段中,删除了 TOOL_RESULT_NUKE_HEAD 和 TOOL_RESULT_NUKE_TAIL 常量,虽然注释提到这些常量未被使用,但需要确认在其他地方是否有依赖于这些常量的逻辑。如果这些常量在未来的功能扩展中可能会被用到,建议保留。此外,删除未使用的代码时,最好进行全面的代码审查,以确保没有潜在的影响。
| const { estimateMessagesTokens, estimateTokens } = require('./compact'); | ||
| const run = this._activeRun(); | ||
| const cfg = vscode.workspace.getConfiguration('deepseekAgent'); | ||
| const provider = cfg.get('provider') || 'deepseek'; |
There was a problem hiding this comment.
在这一段代码中,删除了对 sid 的定义,但没有看到后续代码中对 sid 的使用情况。如果 sid 是后续逻辑中必需的变量,删除它可能导致空指针异常或逻辑错误。请确认 sid 是否在其他地方被使用,或者如果不再需要,确保相关逻辑已被妥善处理。
| clone.id = `s_${Date.now().toString(36)}${_rand4}`; | ||
| clone.title = String(title || `${src.title || 'Fork'} (fork)`).slice(0, 80); | ||
| clone.createdAt = Date.now(); | ||
| clone.updatedAt = Date.now(); |
There was a problem hiding this comment.
在生成 session ID 的过程中,使用 Math.random() 可能导致 ID 的安全性不足。虽然你已经尝试使用 crypto.randomBytes 来增强随机性,但在 catch 块中没有处理异常,可能会导致 _rand4 仍然是默认的 '0000'。建议在 catch 中添加日志记录,以便在 node:crypto 不可用时进行调试。此外,生成 session ID 的逻辑应确保唯一性和不可预测性,考虑使用 crypto.randomUUID() 来生成更安全的 ID。
| const callId = `synthetic_skill_read_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`; | ||
| messages.push({ | ||
| role: 'assistant', | ||
| hrole: 'assistant', |
| // render a real-time usage bar. Cheap to compute since the | ||
| // estimator was just run inside autoCompactIfNeeded above. | ||
| try { | ||
| const ctxTokens = estimateMessagesTokens([{ role: 'system', content: sysPrompt }, ...run.messages]); |
| function updateCtxRing(pct, tokens, win){ | ||
| _lastCtx = { tokens: tokens||0, window: win||0, pct: pct||0 }; | ||
| if (!ftCtxRing || !ftCtxPct) return; | ||
| var p = Math.max(0, Math.min(100, Number(pct)||0)); | ||
| // circumference ≈ 2πr = 2*π*8 ≈ 50.265 | ||
| var C = 50.265; | ||
| ftCtxRing.setAttribute('stroke-dashoffset', String(C * (1 - p/100))); | ||
| ftCtxRing.setAttribute('stroke', p > 85 ? '#e57373' : (p > 65 ? '#ffb74d' : '#66bb6a')); |
| // Issue #142 P3-4: `/context` status report. Breaks down current context | ||
| // usage into system / history / tool-def buckets so the user can decide | ||
| // whether to /compact or /fork. | ||
| async _handleContextCommand() { |
| // Issue #142 P3-5: `/fork [title]` clones the current session under a new | ||
| // id so the user can experiment without polluting the original thread. | ||
| async _handleForkCommand(title) { |
| // Use crypto.randomUUID for session id rather than Math.random to satisfy | ||
| // CodeQL js/insecure-randomness, though session ids are not security-critical. |
| const _crypto = require('crypto'); | ||
| _rand4 = _crypto.randomBytes(2).toString('hex'); | ||
| } catch { /* fallback only if node:crypto unavailable */ } | ||
| clone.id = `s_${Date.now().toString(36)}${_rand4}`; |
| return { | ||
| ...m, | ||
| content: `[deduped — this ${meta.name} of "${meta.key.split('::')[1]}" was re-read later in the conversation; see the later tool result for current contents]`, | ||
| }; |
| const includeMcpTools = vscode.workspace | ||
| .getConfiguration('deepseekAgent') | ||
| .get('includeMcpTools', true); | ||
| const mcpDefs = includeMcpTools ? mcpManager.getToolDefs() : []; | ||
| const allTools = getToolDefs(mcpDefs); |
…D comment - agent-loop.js: hrole -> role in synthetic skill-read assistant message; adapters check msg.role so hrole was silently ignored, breaking skill injection (Copilot review feedback) - session-store.js: update comment to say randomBytes (not randomUUID) to match the actual implementation
|
AI 审查在超大 diff 下质量会下降,建议:
|
| if (fileSize > 50 * 1024) { | ||
| const kb = Math.round(fileSize / 1024); | ||
| const hint = `\n\n[hint] this file is ${kb} KB — consider \`spawn_agent\` (agent_type=explore) for analysis, or \`read_file\` with start_line/end_line to read a focused range, to save context.`; | ||
| return truncate(text) + hint; |
| setTimeout(() => { | ||
| // Guard: the active session may have changed again during the | ||
| // 0-ms delay (rapid clicking). In that case the events are still | ||
| // buffered on `run.events`, so the next _loadSession(id) for this | ||
| // session will replay them; we just no-op here. | ||
| if (run.sessionId !== this._store.sessionId) return; | ||
| this._post({ type: 'replayStart', count: evs.length }); | ||
| for (const ev of evs) this._post(ev); | ||
| this._post({ type: 'replayEnd' }); |
| // Issue #142 P3-4: `/context` status report. Breaks down current context | ||
| // usage into system / history / tool-def buckets so the user can decide | ||
| // whether to /compact or /fork. | ||
| async _handleContextCommand() { |
| // circumference ≈ 2πr = 2*π*8 ≈ 50.265 | ||
| var C = 50.265; | ||
| ftCtxRing.setAttribute('stroke-dashoffset', String(C * (1 - p/100))); | ||
| ftCtxRing.setAttribute('stroke', p > 85 ? '#e57373' : (p > 65 ? '#ffb74d' : '#66bb6a')); | ||
| ftCtxPct.textContent = (p|0) + '%'; |
| return { | ||
| ...m, | ||
| content: `[deduped — this ${meta.name} of "${meta.key.split('::')[1]}" was re-read later in the conversation; see the later tool result for current contents]`, | ||
| }; |
- provider.js (_loadSession): post replayStart immediately, before the setTimeout, so live streamDelta events arriving during the 0-ms delay are also suppressed by webview _replaying flag; close envelope on session-switch no-op to avoid leaving webview stuck in replay-suppress mode (fixes #143 edge case) - file-read.js: reserve hint length in truncate budget so medium-large file output + hint never exceeds MAX_OUTPUT_CHARS - compact.js (grep_search dedup): use greedy regex so Windows drive-letter paths like C:\foo\bar.js:12: dedup correctly - compact.js (read-dedup placeholder): emit structured <name path=... read-collapsed=true/> tag matching PR description - session-store.js (fork): use s_<ts>_<rand> shape with underscore separator, consistent with ensure() - provider.js (/context comment): clarify that the breakdown is system+history only; tool definitions are not included
|
AI 审查在超大 diff 下质量会下降,建议:
|
Summary
This PR rolls up two issues into v0.41.0.
Issue #142 — Context window overhaul
src/chat/compact.js): the most recent tool results are kept verbatim; older turns are collapsed to summary skeletons rather than being dropped wholesale.read_filecalls against the same path keep only the latest payload. Older reads are replaced with<file path=... read-collapsed='true'/>placeholders so the model still knows the file was visited.src/chat/session-store.jspersists / replays these nodes correctly.src/tools/file-read.js): files past a size threshold come back with aread-large-filehint nudging the model to grep first instead of slurping the whole file.Slash commands (
src/chat/provider.js)/compact [focus]— force-compacts the active session immediately.focusbiases the summary; if.deepcopilot/compact.md(orCLAUDE.md) exists in the workspace root, its contents are merged into the focus hint as project-level compaction guidance./context— opens a popover with the current session's token breakdown (system / messages / tools / files / hints)./fork [name]— forks the active session from a chosen message into a fresh session, keeping that context as the new starting point.Footer context ring (
src/webview/html.js,media/chat.js,media/chat.css)#foot .dotstatus dot with#ft-ctx, a ring indicator placed in the bottom-right status bar./context.ctxUsageevents written bysrc/chat/agent-loop.jsafter every turn.Issue #143 — Session-switch flash
Switching away from a running session and switching back caused the chat panel to visibly flash and the scrollbar to jitter. Root cause:
_loadSessionsynchronously replayed every buffered event in a tight loop right afterresetChat(), and every event scheduled its ownrequestAnimationFramescroll-to-bottom.Fix (Phases 1 + 2 from the issue):
retainContextWhenHidden: truewas already set on both webview registration sites insrc/extension.js, so no DOM rebuild is needed for the common case._loadSessionnow defers replay viasetTimeout(..., 0)so the DOM rebuild has time to paint, and brackets the burst withreplayStart/replayEndenvelopes. A guard skips the burst entirely if the active session changed again during the 0 ms delay.media/chat.jsadds a_replayingflag: while true,ascroll()is a no-op. ThereplayEndhandler clears the flag and performs a singlerequestAnimationFramescroll-to-bottom (gated onstick), eliminating the N-RAF burst.Phase 3 (server-side delta coalescing) is intentionally left out — Phases 1 + 2 already remove the visual artefact.
File-by-file
src/chat/compact.jssrc/chat/provider.js/compact,/context,/forkhandlers; deferred replay for Issue #143src/chat/agent-loop.jsctxUsageevent emission, MCP opt-out wiringsrc/chat/session-store.jssrc/tools/file-read.jsread-large-filehintsrc/webview/html.js#ft-ctxring markup,#foot .dotremovedmedia/chat.jsupdateCtxRing,openCtxPop,closeCtxPop),_replayingflag,replayStart/replayEndcasesmedia/chat.css.ft-ctx,.ft-ctx-popstyles; legacy.ctx-usage-barand#foot .dotrules removedsrc/api/anthropic-client.js,src/api/openai-client.js,src/errors.jspackage.jsonREADME.mdVerification
npm run buildpasses (out/extension.js582.9 KB)npx vsce package --no-dependenciesproducesdeep-copilot-0.41.0.vsix(3.85 MB)/compact,/context,/forkexercised; footer ring colour ramps as expected; switching between a busy session and an idle one no longer flashes.Acceptance criteria for #143
npm run buildpasses