mixdog 0.9.93 → 0.9.95
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/NOTICE.md +72 -0
- package/package.json +16 -6
- package/scripts/lib/isolated-root-cleanup.mjs +19 -0
- package/scripts/run-suite.mjs +100 -0
- package/src/lib/rules-builder.cjs +4 -3
- package/src/output-styles/detailed.md +13 -15
- package/src/output-styles/extreme-minimal.md +7 -11
- package/src/output-styles/minimal.md +4 -7
- package/src/output-styles/simple.md +11 -13
- package/src/rules/agent/30-explorer.md +22 -16
- package/src/rules/lead/01-general.md +1 -2
- package/src/rules/lead/lead-brief.md +4 -5
- package/src/rules/lead/lead-tool.md +3 -2
- package/src/rules/shared/01-tool.md +28 -29
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -2
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +23 -7
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -0
- package/src/runtime/agent/orchestrator/agent-trace.mjs +29 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +6 -2
- package/src/runtime/agent/orchestrator/mcp/client.mjs +3 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +97 -7
- package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +11 -4
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +2 -3
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +4 -6
- package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +75 -15
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +8 -8
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +7 -8
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +14 -16
- package/src/runtime/agent/orchestrator/providers/openai-ws-headers.mjs +5 -4
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +10 -10
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -3
- package/src/runtime/agent/orchestrator/providers/registry.mjs +51 -4
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +15 -15
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +12 -32
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +42 -2
- package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +10 -11
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +0 -14
- package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +9 -8
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +7 -4
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +40 -13
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +8 -1
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +5 -9
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +103 -92
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +98 -3
- package/src/runtime/agent/orchestrator/stall-policy.mjs +2 -3
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +25 -3
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -2
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +12 -3
- package/src/runtime/agent/orchestrator/tools/builtin/grep-formatting.mjs +22 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +491 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-output.mjs +91 -13
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +90 -27
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +20 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +19 -9
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +7 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +32 -4
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +16 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +546 -23
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +8 -5
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +10 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +35 -5
- package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +98 -0
- package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +48 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +71 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +7 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +104 -14
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +47 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +6 -17
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +2 -4
- package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +9 -2
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +57 -2
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +151 -18
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +5 -1
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +109 -13
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +10 -10
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +4 -5
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +4 -0
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +1 -1
- package/src/runtime/channels/backends/discord.mjs +5 -14
- package/src/runtime/channels/backends/telegram.mjs +0 -5
- package/src/runtime/channels/lib/inbound-handler.mjs +0 -1
- package/src/runtime/channels/lib/output-forwarder.mjs +24 -5
- package/src/runtime/channels/lib/scheduler.mjs +1 -1
- package/src/runtime/channels/lib/worker-main.mjs +1 -1
- package/src/runtime/media/renditions.mjs +21 -2
- package/src/runtime/memory/lib/tool-call-handler.mjs +16 -1
- package/src/runtime/memory/tool-defs.mjs +6 -6
- package/src/runtime/shared/atomic-file.mjs +53 -0
- package/src/runtime/shared/background-tasks.mjs +16 -6
- package/src/runtime/shared/child-spawn-gate.mjs +50 -26
- package/src/runtime/shared/resource-admission.mjs +40 -1
- package/src/runtime/shared/task-notification-envelope.mjs +11 -2
- package/src/runtime/shared/tool-card-model.mjs +6 -2
- package/src/runtime/shared/tool-status.mjs +10 -1
- package/src/runtime/shared/tool-surface.mjs +5 -0
- package/src/runtime/shared/turn-snapshot.mjs +385 -21
- package/src/runtime/shared/turn-worktree-snapshot.mjs +540 -0
- package/src/session-runtime/context-status.mjs +9 -3
- package/src/session-runtime/lifecycle-api.mjs +35 -5
- package/src/session-runtime/mcp-glue.mjs +11 -6
- package/src/session-runtime/provider-models.mjs +94 -43
- package/src/session-runtime/provider-usage.mjs +26 -2
- package/src/session-runtime/runtime-core.mjs +46 -8
- package/src/session-runtime/runtime-tunables.mjs +4 -0
- package/src/session-runtime/self-update.mjs +33 -4
- package/src/session-runtime/session-lifecycle.mjs +2 -0
- package/src/session-runtime/session-text.mjs +2 -1
- package/src/session-runtime/session-turn-api.mjs +106 -22
- package/src/session-runtime/workflow.mjs +11 -4
- package/src/standalone/agent-tool.mjs +4 -4
- package/src/standalone/backend-daemon.mjs +570 -0
- package/src/standalone/channel-daemon-transport.mjs +141 -1
- package/src/standalone/channel-worker.mjs +3 -2
- package/src/standalone/engine-daemon-client.mjs +894 -0
- package/src/standalone/engine-daemon-local-bridge.mjs +20 -0
- package/src/standalone/engine-daemon-protocol.mjs +33 -0
- package/src/standalone/engine-daemon-service.mjs +864 -0
- package/src/standalone/engine-daemon-transport.mjs +603 -0
- package/src/standalone/explore-tool.mjs +1 -1
- package/src/tui/App.jsx +62 -47
- package/src/tui/app/app-format.mjs +4 -2
- package/src/tui/app/app-view.jsx +5 -0
- package/src/tui/app/channel-pickers.mjs +7 -6
- package/src/tui/app/core-memory-picker.mjs +4 -4
- package/src/tui/app/extension-pickers.mjs +20 -18
- package/src/tui/app/maintenance-pickers.mjs +27 -27
- package/src/tui/app/onboarding-steps.mjs +23 -18
- package/src/tui/app/prompt-submit.mjs +13 -2
- package/src/tui/app/route-pickers.mjs +13 -8
- package/src/tui/app/settings-picker.mjs +55 -58
- package/src/tui/app/slash-dispatch.mjs +32 -28
- package/src/tui/app/transcript-window.mjs +19 -0
- package/src/tui/app/usage-context-panels.mjs +22 -7
- package/src/tui/app/use-mouse-input.mjs +25 -3
- package/src/tui/app/use-prompt-queue-history.mjs +31 -16
- package/src/tui/app/use-transcript-scroll.mjs +30 -8
- package/src/tui/app/use-transcript-window.mjs +22 -1
- package/src/tui/app/use-welcome-prompt-hint.mjs +2 -2
- package/src/tui/components/PromptInput.jsx +10 -0
- package/src/tui/components/Spinner.jsx +89 -86
- package/src/tui/components/TextEntryPanel.jsx +14 -0
- package/src/tui/components/ToolExecution.jsx +2 -2
- package/src/tui/dist/index.mjs +1537 -9718
- package/src/tui/engine/agent-job-feed.mjs +2 -2
- package/src/tui/engine/live-share.mjs +97 -4
- package/src/tui/engine/session-api-ext.mjs +23 -1
- package/src/tui/engine/session-api.mjs +88 -41
- package/src/tui/engine/session-flow.mjs +43 -4
- package/src/tui/engine/tool-card-results.mjs +6 -0
- package/src/tui/engine/turn.mjs +84 -4
- package/src/tui/engine-local-session.mjs +1108 -0
- package/src/tui/engine.mjs +16 -1057
- package/src/tui/index.jsx +47 -2
- package/src/tui/markdown/stream-fence.mjs +1 -1
- package/src/tui/spinner-meta.mjs +80 -0
- package/src/tui/spinner-verbs.mjs +35 -0
- package/src/ui/statusline-segments.mjs +43 -10
- package/src/ui/statusline.mjs +10 -1
- package/scripts/tmp-cdp-errors.mjs +0 -41
- package/scripts/tmp-cdp-inspect.mjs +0 -41
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +0 -176
- package/src/standalone/channel-daemon.mjs +0 -226
|
@@ -104,7 +104,7 @@ export const BUILTIN_TOOLS = [
|
|
|
104
104
|
type: 'object',
|
|
105
105
|
properties: {
|
|
106
106
|
task_id: { type: 'string', description: 'shell async task_id.' },
|
|
107
|
-
action: { type: 'string', enum: ['list', 'status', 'read', 'wait', 'cancel'], description: '
|
|
107
|
+
action: { type: 'string', enum: ['list', 'status', 'read', 'wait', 'cancel'], description: 'Defaults to list, or wait when task_id is set; avoid polling loops.' },
|
|
108
108
|
timeout_ms: { type: 'number', description: 'Wait timeout ms.' },
|
|
109
109
|
},
|
|
110
110
|
required: [],
|
|
@@ -115,7 +115,7 @@ export const BUILTIN_TOOLS = [
|
|
|
115
115
|
name: 'grep',
|
|
116
116
|
title: 'Mixdog Grep',
|
|
117
117
|
annotations: { title: 'Mixdog Grep', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
|
|
118
|
-
description: 'Content search by literal or regex over file/dir scopes.',
|
|
118
|
+
description: 'Content search by literal or regex over file/dir scopes; matches return copy-ready raw source spans.',
|
|
119
119
|
inputSchema: {
|
|
120
120
|
type: 'object',
|
|
121
121
|
properties: {
|
|
@@ -143,7 +143,7 @@ export const BUILTIN_TOOLS = [
|
|
|
143
143
|
output_mode: { type: 'string', enum: ['content_with_context', 'files_with_matches', 'count'], description: 'content_with_context (default); files_with_matches/count for existence.' },
|
|
144
144
|
head_limit: { type: 'number', minimum: 0, description: 'Max results. Defaults to 250; 0 = unlimited.' },
|
|
145
145
|
offset: { type: 'number', minimum: 0, description: 'Skip results for paging.' },
|
|
146
|
-
|
|
146
|
+
context: { type: 'number', minimum: 0, description: 'Minimum surrounding lines; runtime expands within its call budget. Omit for automatic context; 0 for matches only.' },
|
|
147
147
|
},
|
|
148
148
|
anyOf: [
|
|
149
149
|
{ required: ['pattern'] },
|
|
@@ -197,7 +197,7 @@ export const BUILTIN_TOOLS = [
|
|
|
197
197
|
description: 'Partial path/name words; query[] batches.',
|
|
198
198
|
},
|
|
199
199
|
path: { type: 'string', description: 'Base directory.' },
|
|
200
|
-
head_limit: { type: 'number', description: 'Max paths. Defaults to 25.' },
|
|
200
|
+
head_limit: { type: 'number', description: 'Max paths across the call. Defaults to 25.' },
|
|
201
201
|
},
|
|
202
202
|
required: ['query'],
|
|
203
203
|
additionalProperties: false,
|
|
@@ -207,7 +207,7 @@ export const BUILTIN_TOOLS = [
|
|
|
207
207
|
name: 'list',
|
|
208
208
|
title: 'Mixdog List Directory',
|
|
209
209
|
annotations: { title: 'Mixdog List Directory', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
|
|
210
|
-
description: 'List directory entries; batch independent dirs as path[].',
|
|
210
|
+
description: 'List directory entries (path + type); batch independent dirs as path[].',
|
|
211
211
|
inputSchema: {
|
|
212
212
|
type: 'object',
|
|
213
213
|
properties: {
|
|
@@ -94,7 +94,13 @@ function fuzzyScore(query, str) {
|
|
|
94
94
|
// order but scattered mid-word, no contiguity/boundary structure) is treated
|
|
95
95
|
// as noise rather than a real hit. Contiguous substring / basename matches
|
|
96
96
|
// bypass the floor entirely — an exact hit must never be filtered.
|
|
97
|
-
|
|
97
|
+
// Measured separation for scattered junk vs real hits ("readme" probe):
|
|
98
|
+
// genuine matches score ≥8/char (boundary + contiguity structure), while
|
|
99
|
+
// incidental subsequences on unrelated paths land at 5–6/char — 6.5 splits
|
|
100
|
+
// them. The floor is tested against the RAW subsequence score (see
|
|
101
|
+
// fuzzyRank), never the +40 basename rank bonus, which otherwise vaults
|
|
102
|
+
// weak basename subsequences straight past any floor.
|
|
103
|
+
const SUBSEQUENCE_MIN_PER_CHAR = 6.5;
|
|
98
104
|
|
|
99
105
|
// Separator/case-insensitive normalization used for the "contiguous substring"
|
|
100
106
|
// strong-match test. Mirrors the query normalization in fuzzyScore so
|
|
@@ -163,7 +169,7 @@ export function fuzzyRank(query, items, limit = 0) {
|
|
|
163
169
|
const score = prepareFuzzyScore(query);
|
|
164
170
|
// Floor scales with query length: a scattered subsequence earns ~1 point
|
|
165
171
|
// per char, while any contiguous run (+5/char) or word-boundary hit
|
|
166
|
-
// (+8) pushes a genuine match well past
|
|
172
|
+
// (+8) pushes a genuine match well past 6.5/char.
|
|
167
173
|
const floor = normQuery.length * SUBSEQUENCE_MIN_PER_CHAR;
|
|
168
174
|
const scored = [];
|
|
169
175
|
// Preserve slice's legacy coercion behavior for non-integer public callers;
|
|
@@ -185,7 +191,10 @@ export function fuzzyRank(query, items, limit = 0) {
|
|
|
185
191
|
// Otherwise it is subsequence-only: keep it only if it clears the
|
|
186
192
|
// per-char floor. Weak scattered matches (the pgAdmin-style junk that
|
|
187
193
|
// merely contains the query chars in order) fall below it and drop out.
|
|
188
|
-
|
|
194
|
+
// Compare the RAW subsequence score — the +40 basename rank bonus is
|
|
195
|
+
// for ordering only and must not lift junk over the quality floor.
|
|
196
|
+
const rawSc = Math.max(pathScore ?? -Infinity, baseScore ?? -Infinity);
|
|
197
|
+
if (!strong && rawSc < floor) continue;
|
|
189
198
|
const entry = { item, score: sc };
|
|
190
199
|
if (bounded) retainTopRanked(scored, entry, ordinal, limit);
|
|
191
200
|
else scored.push(entry);
|
|
@@ -43,6 +43,28 @@ export function splitGrepCountPrefix(line) {
|
|
|
43
43
|
return { path: text.slice(0, idx), pathEnd: idx, count: Number(count) };
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
export function parseGrepContextHeader(line) {
|
|
47
|
+
const match = /^# (.+):(\d+) \[lines (\d+)-(\d+)\]$/.exec(String(line || ''));
|
|
48
|
+
if (!match) return null;
|
|
49
|
+
const matchLine = Number(match[2]);
|
|
50
|
+
const startLine = Number(match[3]);
|
|
51
|
+
const endLine = Number(match[4]);
|
|
52
|
+
if (!Number.isSafeInteger(matchLine)
|
|
53
|
+
|| !Number.isSafeInteger(startLine)
|
|
54
|
+
|| !Number.isSafeInteger(endLine)
|
|
55
|
+
|| startLine < 1
|
|
56
|
+
|| endLine < startLine
|
|
57
|
+
|| matchLine < startLine
|
|
58
|
+
|| matchLine > endLine) return null;
|
|
59
|
+
return {
|
|
60
|
+
path: match[1],
|
|
61
|
+
matchLine,
|
|
62
|
+
startLine,
|
|
63
|
+
endLine,
|
|
64
|
+
sourceLineCount: endLine - startLine + 1,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
46
68
|
export function normalizeGrepLine(line, pathOnly = false, outputMode = 'content', filenameOmitted = false) {
|
|
47
69
|
if (process.platform !== 'win32') return line;
|
|
48
70
|
// files_with_matches mode: the WHOLE line is a path, so every separator is
|
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import { createReadStream } from 'node:fs';
|
|
2
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
3
|
+
import { createInterface } from 'node:readline';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
normalizeOutputPath,
|
|
7
|
+
} from '../path-utils.mjs';
|
|
8
|
+
import {
|
|
9
|
+
splitGrepLineNumberOnlyPrefix,
|
|
10
|
+
splitGrepLinePrefix,
|
|
11
|
+
} from '../grep-formatting.mjs';
|
|
12
|
+
import {
|
|
13
|
+
relativePathPrefix,
|
|
14
|
+
} from '../search-path-diagnostics.mjs';
|
|
15
|
+
import {
|
|
16
|
+
relativeGrepLine,
|
|
17
|
+
} from './search-input-helpers.mjs';
|
|
18
|
+
|
|
19
|
+
export const GREP_CONTEXT_CHAR_BUDGET_DEFAULT = 10_000;
|
|
20
|
+
const GREP_CONTEXT_READ_CONCURRENCY = 4;
|
|
21
|
+
const GREP_FOCUSED_CONTEXT_RADIUS = 12;
|
|
22
|
+
const GREP_FOCUSED_RAW_BLOCKS = 3;
|
|
23
|
+
// Anchors must stay usable as evidence without a follow-up read: keep the
|
|
24
|
+
// complete match line and only cut pathological long lines (minified files).
|
|
25
|
+
// Mid-line ellipsis below ~100 chars measurably pushed models into re-read
|
|
26
|
+
// verification loops (tool-budget bench, 20260805).
|
|
27
|
+
const GREP_COMPACT_ANCHOR_CONTENT_MAX = 400;
|
|
28
|
+
|
|
29
|
+
function parseAnchor(line, {
|
|
30
|
+
workDir,
|
|
31
|
+
rgSpawnCwd,
|
|
32
|
+
grepResolvedPath,
|
|
33
|
+
searchPath,
|
|
34
|
+
outputMode,
|
|
35
|
+
filenameOmitted,
|
|
36
|
+
}) {
|
|
37
|
+
if (filenameOmitted) {
|
|
38
|
+
const split = splitGrepLineNumberOnlyPrefix(line);
|
|
39
|
+
if (!split || split.delimiter !== ':') return null;
|
|
40
|
+
return {
|
|
41
|
+
path: relativePathPrefix(normalizeOutputPath(searchPath), workDir),
|
|
42
|
+
absolutePath: grepResolvedPath,
|
|
43
|
+
lineNo: split.lineNo,
|
|
44
|
+
content: String(line).slice(split.markerEnd),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const raw = splitGrepLinePrefix(line);
|
|
48
|
+
if (!raw || raw.delimiter !== ':') return null;
|
|
49
|
+
const normalized = relativeGrepLine(line, workDir, false, outputMode, false);
|
|
50
|
+
const display = splitGrepLinePrefix(normalized);
|
|
51
|
+
if (!display || display.delimiter !== ':') return null;
|
|
52
|
+
return {
|
|
53
|
+
path: display.path,
|
|
54
|
+
absolutePath: isAbsolute(raw.path) ? raw.path : resolve(rgSpawnCwd, raw.path),
|
|
55
|
+
lineNo: raw.lineNo,
|
|
56
|
+
content: String(line).slice(raw.markerEnd),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseAnchors(allLines, options) {
|
|
61
|
+
const anchors = [];
|
|
62
|
+
const seen = new Set();
|
|
63
|
+
for (const line of allLines) {
|
|
64
|
+
const anchor = parseAnchor(line, options);
|
|
65
|
+
if (!anchor) continue;
|
|
66
|
+
const key = `${anchor.absolutePath}\0${anchor.lineNo}`;
|
|
67
|
+
if (seen.has(key)) continue;
|
|
68
|
+
seen.add(key);
|
|
69
|
+
anchors.push({ ...anchor, order: anchors.length });
|
|
70
|
+
}
|
|
71
|
+
return anchors;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function splitTopLevelAlternatives(pattern) {
|
|
75
|
+
const source = String(pattern || '');
|
|
76
|
+
const parts = [];
|
|
77
|
+
let start = 0;
|
|
78
|
+
let depth = 0;
|
|
79
|
+
let inClass = false;
|
|
80
|
+
let escaped = false;
|
|
81
|
+
for (let index = 0; index < source.length; index++) {
|
|
82
|
+
const char = source[index];
|
|
83
|
+
if (escaped) {
|
|
84
|
+
escaped = false;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (char === '\\') {
|
|
88
|
+
escaped = true;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (inClass) {
|
|
92
|
+
if (char === ']') inClass = false;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (char === '[') {
|
|
96
|
+
inClass = true;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (char === '(') {
|
|
100
|
+
depth++;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (char === ')' && depth > 0) {
|
|
104
|
+
depth--;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (char === '|' && depth === 0) {
|
|
108
|
+
parts.push(source.slice(start, index));
|
|
109
|
+
start = index + 1;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (parts.length === 0) return [source];
|
|
113
|
+
parts.push(source.slice(start));
|
|
114
|
+
return parts;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function rankAnchors(anchors, patterns, caseInsensitive) {
|
|
118
|
+
const branches = [];
|
|
119
|
+
for (const pattern of Array.isArray(patterns) ? patterns : [patterns]) {
|
|
120
|
+
for (const branch of splitTopLevelAlternatives(pattern)) {
|
|
121
|
+
if (!branch) continue;
|
|
122
|
+
try {
|
|
123
|
+
branches.push(new RegExp(branch, caseInsensitive ? 'i' : ''));
|
|
124
|
+
} catch {
|
|
125
|
+
// PCRE2/ripgrep syntax is not always valid JavaScript regex.
|
|
126
|
+
// Keep source order when a branch cannot be ranked safely.
|
|
127
|
+
branches.push(null);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (branches.length <= 1) return anchors;
|
|
132
|
+
return anchors.map((anchor) => ({
|
|
133
|
+
...anchor,
|
|
134
|
+
priority: branches.findIndex((branch) => branch?.test(anchor.content)),
|
|
135
|
+
})).map((anchor) => ({
|
|
136
|
+
...anchor,
|
|
137
|
+
priority: anchor.priority < 0 ? Number.MAX_SAFE_INTEGER : anchor.priority,
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function selectAnchors(anchors, headLimit, offset) {
|
|
142
|
+
const afterOffset = offset > 0 ? anchors.slice(offset) : anchors;
|
|
143
|
+
if (afterOffset.length === 0) {
|
|
144
|
+
return { selected: [], shown: 0, omitted: 0, nextOffset: offset };
|
|
145
|
+
}
|
|
146
|
+
const shown = headLimit === Infinity ? afterOffset.length : Math.min(headLimit, afterOffset.length);
|
|
147
|
+
const omitted = afterOffset.length - shown;
|
|
148
|
+
if (omitted <= 0 || shown <= 0) {
|
|
149
|
+
return {
|
|
150
|
+
selected: afterOffset.slice(0, shown),
|
|
151
|
+
shown,
|
|
152
|
+
omitted,
|
|
153
|
+
nextOffset: offset + shown,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const headCount = Math.max(1, Math.ceil(shown / 2));
|
|
157
|
+
const tailCount = shown - headCount;
|
|
158
|
+
return {
|
|
159
|
+
selected: [
|
|
160
|
+
...afterOffset.slice(0, headCount),
|
|
161
|
+
...(tailCount > 0 ? afterOffset.slice(afterOffset.length - tailCount) : []),
|
|
162
|
+
],
|
|
163
|
+
shown,
|
|
164
|
+
omitted,
|
|
165
|
+
nextOffset: offset + headCount,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function mergeIntervals(intervals) {
|
|
170
|
+
const sorted = [...intervals].sort((left, right) => left.start - right.start || left.end - right.end);
|
|
171
|
+
const merged = [];
|
|
172
|
+
for (const interval of sorted) {
|
|
173
|
+
const previous = merged.at(-1);
|
|
174
|
+
if (previous && interval.start <= previous.end + 1) {
|
|
175
|
+
previous.end = Math.max(previous.end, interval.end);
|
|
176
|
+
} else {
|
|
177
|
+
merged.push({ ...interval });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return merged;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function readFileWindows(entry, radius, signal) {
|
|
184
|
+
const intervals = mergeIntervals(entry.anchors.map((anchor) => ({
|
|
185
|
+
start: Math.max(1, anchor.lineNo - radius),
|
|
186
|
+
end: anchor.lineNo + radius,
|
|
187
|
+
})));
|
|
188
|
+
const lines = new Map();
|
|
189
|
+
if (intervals.length === 0) return lines;
|
|
190
|
+
const input = createReadStream(entry.absolutePath, { encoding: 'utf8' });
|
|
191
|
+
const reader = createInterface({ input, crlfDelay: Infinity });
|
|
192
|
+
let lineNo = 0;
|
|
193
|
+
let intervalIndex = 0;
|
|
194
|
+
const abort = () => input.destroy(Object.assign(new Error('grep context expansion aborted'), { code: 'ABORT_ERR' }));
|
|
195
|
+
if (signal) {
|
|
196
|
+
if (signal.aborted) abort();
|
|
197
|
+
else signal.addEventListener('abort', abort, { once: true });
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
for await (const line of reader) {
|
|
201
|
+
lineNo++;
|
|
202
|
+
while (intervalIndex < intervals.length && lineNo > intervals[intervalIndex].end) intervalIndex++;
|
|
203
|
+
if (intervalIndex >= intervals.length) {
|
|
204
|
+
input.destroy();
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
const interval = intervals[intervalIndex];
|
|
208
|
+
if (lineNo >= interval.start && lineNo <= interval.end) lines.set(lineNo, line);
|
|
209
|
+
}
|
|
210
|
+
return lines;
|
|
211
|
+
} finally {
|
|
212
|
+
if (signal) signal.removeEventListener('abort', abort);
|
|
213
|
+
reader.close();
|
|
214
|
+
input.destroy();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function readAnchorSources(anchors, radius, signal) {
|
|
219
|
+
const groups = new Map();
|
|
220
|
+
for (const anchor of anchors) {
|
|
221
|
+
if (!groups.has(anchor.absolutePath)) {
|
|
222
|
+
groups.set(anchor.absolutePath, {
|
|
223
|
+
absolutePath: anchor.absolutePath,
|
|
224
|
+
anchors: [],
|
|
225
|
+
lines: new Map(),
|
|
226
|
+
error: null,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
groups.get(anchor.absolutePath).anchors.push(anchor);
|
|
230
|
+
}
|
|
231
|
+
const entries = [...groups.values()];
|
|
232
|
+
let next = 0;
|
|
233
|
+
const worker = async () => {
|
|
234
|
+
while (next < entries.length) {
|
|
235
|
+
const entry = entries[next++];
|
|
236
|
+
try {
|
|
237
|
+
entry.lines = await readFileWindows(entry, radius, signal);
|
|
238
|
+
} catch (err) {
|
|
239
|
+
entry.error = err;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
await Promise.all(Array.from(
|
|
244
|
+
{ length: Math.min(GREP_CONTEXT_READ_CONCURRENCY, entries.length) },
|
|
245
|
+
() => worker(),
|
|
246
|
+
));
|
|
247
|
+
return groups;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function sourceBlock(anchor, source, radius) {
|
|
251
|
+
if (!source || source.error || !source.lines.has(anchor.lineNo)) {
|
|
252
|
+
return {
|
|
253
|
+
path: anchor.path,
|
|
254
|
+
matchLine: anchor.lineNo,
|
|
255
|
+
startLine: anchor.lineNo,
|
|
256
|
+
endLine: anchor.lineNo,
|
|
257
|
+
contents: [anchor.content],
|
|
258
|
+
sourceComplete: false,
|
|
259
|
+
priority: anchor.priority ?? Number.MAX_SAFE_INTEGER,
|
|
260
|
+
order: anchor.order ?? Number.MAX_SAFE_INTEGER,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
let startLine = Math.max(1, anchor.lineNo - radius);
|
|
264
|
+
let endLine = anchor.lineNo + radius;
|
|
265
|
+
while (startLine < anchor.lineNo && !source.lines.has(startLine)) startLine++;
|
|
266
|
+
while (endLine > anchor.lineNo && !source.lines.has(endLine)) endLine--;
|
|
267
|
+
const contents = [];
|
|
268
|
+
for (let lineNo = startLine; lineNo <= endLine; lineNo++) {
|
|
269
|
+
if (!source.lines.has(lineNo)) break;
|
|
270
|
+
contents.push(source.lines.get(lineNo));
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
path: anchor.path,
|
|
274
|
+
matchLine: anchor.lineNo,
|
|
275
|
+
startLine,
|
|
276
|
+
endLine: startLine + contents.length - 1,
|
|
277
|
+
contents,
|
|
278
|
+
sourceComplete: true,
|
|
279
|
+
priority: anchor.priority ?? Number.MAX_SAFE_INTEGER,
|
|
280
|
+
order: anchor.order ?? Number.MAX_SAFE_INTEGER,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function mergeBlocks(blocks) {
|
|
285
|
+
const sorted = [...blocks].sort((left, right) => (
|
|
286
|
+
left.path.localeCompare(right.path)
|
|
287
|
+
|| left.startLine - right.startLine
|
|
288
|
+
|| left.endLine - right.endLine
|
|
289
|
+
));
|
|
290
|
+
const merged = [];
|
|
291
|
+
for (const block of sorted) {
|
|
292
|
+
const previous = merged.at(-1);
|
|
293
|
+
if (previous
|
|
294
|
+
&& previous.path === block.path
|
|
295
|
+
&& block.startLine <= previous.endLine + 1) {
|
|
296
|
+
const overlap = Math.max(0, previous.endLine - block.startLine + 1);
|
|
297
|
+
previous.contents.push(...block.contents.slice(overlap));
|
|
298
|
+
previous.endLine = Math.max(previous.endLine, block.endLine);
|
|
299
|
+
previous.sourceComplete = previous.sourceComplete && block.sourceComplete;
|
|
300
|
+
if (block.priority < previous.priority
|
|
301
|
+
|| (block.priority === previous.priority && block.order < previous.order)) {
|
|
302
|
+
previous.matchLine = block.matchLine;
|
|
303
|
+
previous.priority = block.priority;
|
|
304
|
+
previous.order = block.order;
|
|
305
|
+
}
|
|
306
|
+
} else {
|
|
307
|
+
merged.push({ ...block, contents: [...block.contents] });
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return merged.sort((left, right) => (
|
|
311
|
+
left.priority - right.priority
|
|
312
|
+
|| left.order - right.order
|
|
313
|
+
));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function pagingNotice({ shown, total, totalKnown, omitted, offset, nextOffset }) {
|
|
317
|
+
const totalStr = totalKnown ? `${total}` : `>=${total}`;
|
|
318
|
+
if (shown === 0) {
|
|
319
|
+
return totalKnown
|
|
320
|
+
? `[Showing 0 of ${totalStr} matches; offset ${offset} past end]`
|
|
321
|
+
: `[Showing 0 of ${totalStr} matches (results partial); offset ${offset} is beyond the streamed window — matches past it may exist. Narrow path/glob/pattern instead of paging deeper.]`;
|
|
322
|
+
}
|
|
323
|
+
return (omitted > 0 || !totalKnown)
|
|
324
|
+
? `\n[Showing ${shown} of ${totalStr} matches${totalKnown ? '' : ' (results partial)'}; pass offset:${nextOffset} for more]`
|
|
325
|
+
: '';
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function renderAtRadius(selected, sources, radius, _budget, notice) {
|
|
329
|
+
const blocks = mergeBlocks(selected.map((anchor) => (
|
|
330
|
+
sourceBlock(anchor, sources.get(anchor.absolutePath), radius)
|
|
331
|
+
)));
|
|
332
|
+
const body = blocks.map((block) => (
|
|
333
|
+
`# ${block.path}:${block.matchLine} [lines ${block.startLine}-${block.endLine}]\n${block.contents.join('\n')}`
|
|
334
|
+
)).join('\n');
|
|
335
|
+
const sourceComplete = blocks.every((block) => block.sourceComplete);
|
|
336
|
+
// Header is a format cue only — internal radius/char-budget numbers are
|
|
337
|
+
// harness noise and stay out of the model-visible text.
|
|
338
|
+
const text = `[Raw source spans; apply_patch context may be copied verbatim]\n${body}${notice}`;
|
|
339
|
+
return { text, sourceComplete, blockCount: blocks.length };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function anchorPriority(left, right) {
|
|
343
|
+
return (left.priority ?? Number.MAX_SAFE_INTEGER) - (right.priority ?? Number.MAX_SAFE_INTEGER)
|
|
344
|
+
|| (left.order ?? Number.MAX_SAFE_INTEGER) - (right.order ?? Number.MAX_SAFE_INTEGER);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function compactAnchorContent(content) {
|
|
348
|
+
const normalized = String(content || '').trim().replace(/\s+/g, ' ');
|
|
349
|
+
return normalized.length <= GREP_COMPACT_ANCHOR_CONTENT_MAX
|
|
350
|
+
? normalized
|
|
351
|
+
: `${normalized.slice(0, GREP_COMPACT_ANCHOR_CONTENT_MAX - 1)}…`;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function anchorRangeHint(anchor, radius) {
|
|
355
|
+
const startLine = Math.max(1, anchor.lineNo - radius);
|
|
356
|
+
const endLine = anchor.lineNo + radius;
|
|
357
|
+
return `lines ${startLine}-${endLine}`;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function renderFocusedContext(selected, sources, radius, _budget, notice) {
|
|
361
|
+
const ordered = [...selected].sort(anchorPriority);
|
|
362
|
+
const rankedBlocks = mergeBlocks(ordered.map((anchor) => (
|
|
363
|
+
sourceBlock(anchor, sources.get(anchor.absolutePath), radius)
|
|
364
|
+
)));
|
|
365
|
+
const rawBlocks = rankedBlocks.slice(0, GREP_FOCUSED_RAW_BLOCKS);
|
|
366
|
+
const covered = (anchor) => rawBlocks.some((block) => (
|
|
367
|
+
anchor.path === block.path
|
|
368
|
+
&& anchor.lineNo >= block.startLine
|
|
369
|
+
&& anchor.lineNo <= block.endLine
|
|
370
|
+
));
|
|
371
|
+
const compact = ordered.filter((anchor) => !covered(anchor));
|
|
372
|
+
const raw = rawBlocks.map((block) => (
|
|
373
|
+
`# ${block.path}:${block.matchLine} [lines ${block.startLine}-${block.endLine}]\n${block.contents.join('\n')}`
|
|
374
|
+
)).join('\n');
|
|
375
|
+
const anchors = compact.length
|
|
376
|
+
? `\n# Additional matches\n${compact.map((anchor) => (
|
|
377
|
+
`${anchor.path}:${anchor.lineNo}:${compactAnchorContent(anchor.content)} [${anchorRangeHint(anchor, radius)}]`
|
|
378
|
+
)).join('\n')}`
|
|
379
|
+
: '';
|
|
380
|
+
const header = compact.length
|
|
381
|
+
? `[Top ${rawBlocks.length} of ${ordered.length}; remaining as path:line anchors]`
|
|
382
|
+
: null;
|
|
383
|
+
const text = `${header ? `${header}\n` : ''}${raw}${anchors}${notice}`;
|
|
384
|
+
return {
|
|
385
|
+
text,
|
|
386
|
+
sourceComplete: rawBlocks.every((block) => block.sourceComplete),
|
|
387
|
+
blockCount: rawBlocks.length,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export async function expandGrepAnchorContextOutput({
|
|
392
|
+
allLines,
|
|
393
|
+
workDir,
|
|
394
|
+
rgSpawnCwd,
|
|
395
|
+
grepResolvedPath,
|
|
396
|
+
searchPath,
|
|
397
|
+
outputMode,
|
|
398
|
+
filenameOmitted,
|
|
399
|
+
headLimit,
|
|
400
|
+
offset,
|
|
401
|
+
totalKnown = true,
|
|
402
|
+
requestedContext,
|
|
403
|
+
maxContext,
|
|
404
|
+
patterns = [],
|
|
405
|
+
caseInsensitive = false,
|
|
406
|
+
charBudget = GREP_CONTEXT_CHAR_BUDGET_DEFAULT,
|
|
407
|
+
signal,
|
|
408
|
+
}) {
|
|
409
|
+
const anchors = rankAnchors(parseAnchors(allLines, {
|
|
410
|
+
workDir,
|
|
411
|
+
rgSpawnCwd,
|
|
412
|
+
grepResolvedPath,
|
|
413
|
+
searchPath,
|
|
414
|
+
outputMode,
|
|
415
|
+
filenameOmitted,
|
|
416
|
+
}), patterns, caseInsensitive);
|
|
417
|
+
const total = anchors.length;
|
|
418
|
+
if (total === 0) return { text: '', total: 0, shown: 0, omitted: 0, sourceComplete: true };
|
|
419
|
+
const window = selectAnchors(anchors, headLimit, offset);
|
|
420
|
+
let notice = pagingNotice({
|
|
421
|
+
...window,
|
|
422
|
+
total,
|
|
423
|
+
totalKnown,
|
|
424
|
+
offset,
|
|
425
|
+
});
|
|
426
|
+
if (window.selected.length === 0) {
|
|
427
|
+
return {
|
|
428
|
+
text: notice,
|
|
429
|
+
total,
|
|
430
|
+
shown: 0,
|
|
431
|
+
omitted: 0,
|
|
432
|
+
sourceComplete: true,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
const requested = Math.max(0, Math.floor(Number(requestedContext) || 0));
|
|
436
|
+
const target = Math.max(requested, Math.max(0, Math.floor(Number(maxContext) || 0)));
|
|
437
|
+
const budget = Math.max(512, Math.floor(Number(charBudget) || GREP_CONTEXT_CHAR_BUDGET_DEFAULT));
|
|
438
|
+
const sources = await readAnchorSources(window.selected, target, signal);
|
|
439
|
+
let selected = window.selected;
|
|
440
|
+
let shown = window.shown;
|
|
441
|
+
let omitted = window.omitted;
|
|
442
|
+
let radius = target;
|
|
443
|
+
let rendered = renderAtRadius(selected, sources, radius, budget, notice);
|
|
444
|
+
// Progressive disclosure: one or two source clusters that fit are most
|
|
445
|
+
// useful as patch-ready raw text. Broad searches instead expand up to three
|
|
446
|
+
// high-priority source clusters and retain the rest as compact path:line
|
|
447
|
+
// anchors with neutral range metadata. This avoids paying 25 lines for
|
|
448
|
+
// every match without nudging the model into unnecessary follow-up reads.
|
|
449
|
+
const sparseRaw = rendered.blockCount <= 2 && rendered.text.length <= budget;
|
|
450
|
+
if (!sparseRaw) {
|
|
451
|
+
radius = Math.min(target, GREP_FOCUSED_CONTEXT_RADIUS);
|
|
452
|
+
rendered = renderFocusedContext(selected, sources, radius, budget, notice);
|
|
453
|
+
while (rendered.text.length > budget && radius > 0) {
|
|
454
|
+
radius--;
|
|
455
|
+
rendered = renderFocusedContext(selected, sources, radius, budget, notice);
|
|
456
|
+
}
|
|
457
|
+
} else {
|
|
458
|
+
return {
|
|
459
|
+
...rendered,
|
|
460
|
+
total,
|
|
461
|
+
shown,
|
|
462
|
+
omitted,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
while (rendered.text.length > budget && selected.length > 1) {
|
|
466
|
+
selected = [...selected].sort(anchorPriority).slice(0, -1);
|
|
467
|
+
shown = selected.length;
|
|
468
|
+
omitted = Math.max(0, total - offset - shown);
|
|
469
|
+
notice = pagingNotice({
|
|
470
|
+
shown,
|
|
471
|
+
total,
|
|
472
|
+
totalKnown,
|
|
473
|
+
omitted,
|
|
474
|
+
offset,
|
|
475
|
+
nextOffset: offset + shown,
|
|
476
|
+
});
|
|
477
|
+
rendered = renderFocusedContext(
|
|
478
|
+
selected,
|
|
479
|
+
sources,
|
|
480
|
+
radius,
|
|
481
|
+
budget,
|
|
482
|
+
notice,
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
return {
|
|
486
|
+
...rendered,
|
|
487
|
+
total,
|
|
488
|
+
shown,
|
|
489
|
+
omitted,
|
|
490
|
+
};
|
|
491
|
+
}
|