Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
c8e986c
feat(context): condense context after idle time
DavidWAbrahams Nov 6, 2026
0ad4da0
clamp the to conext threshold sliders together
DavidWAbrahams Nov 6, 2026
4c42cd1
nest idle condensing under auto condense and reword idle description
DavidWAbrahams Nov 6, 2026
5e97895
Merge branch 'main' into feature/idle-condensing
DavidWAbrahams Nov 7, 2026
6df461e
fix formatting and repair eslint-suppressions.json merge corruption
DavidWAbrahams Nov 7, 2026
6710459
fix Task.spec.ts condense stub to resolve with CondenseContextResult
DavidWAbrahams Nov 7, 2026
19952ce
test webview-ui idle-condense state and condense queueing for patch c…
DavidWAbrahams Nov 7, 2026
93f0d4f
fix: address review findings on idle-condense clamping, inheritance, …
DavidWAbrahams Nov 8, 2026
bf6e469
Update webview-ui/src/components/settings/ContextManagementSettings.tsx
DavidWAbrahams Nov 8, 2026
731f803
fix: type idle-condensing checkbox change event with checkbox target …
DavidWAbrahams Nov 8, 2026
1b1fbeb
fix: slice long idle timer delays and reset countdown on every edit
DavidWAbrahams Nov 8, 2026
bf9d892
style: apply prettier formatting to idle-condense sources
DavidWAbrahams Nov 8, 2026
ae763be
test: define abort as own property in idle-condense task stub
DavidWAbrahams Nov 8, 2026
2eecdb3
test: redefine abort instead of assigning in aborted-task test
DavidWAbrahams Nov 9, 2026
c617e45
Merge branch 'main' into feature/idle-condensing
DavidWAbrahams Nov 9, 2026
3a7e6ef
test: cover idle-condensing patch gaps flagged by codecov
DavidWAbrahams Nov 9, 2026
c353db3
fix: re-arm idle countdown with newly saved delay
DavidWAbrahams Nov 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,38 @@ export const globalSettingsSchema = z.object({
allowedMaxCost: z.number().nullish(),
autoCondenseContext: z.boolean().optional(),
autoCondenseContextPercent: z.number().optional(),
/**
* Master toggle for idle condensing: opportunistically condense the context
* while the task is waiting on the user, once the context exceeds the
* (typically lower) idle trigger threshold and the idle time has elapsed.
* @default false
*/
idleCondenseContext: z.boolean().optional(),
/**
* Context-window percentage (relative to the condensing context window) at
* or above which an idle condense is allowed to run. Must not exceed the
* effective autoCondenseContextPercent.
* @default 50
*/
idleCondenseContextPercent: z.number().optional(),
/**
* Seconds of waiting-on-user (idle) time before an idle condense runs.
* The countdown restarts when the user types in the prompt input.
* @default 180
*/
idleCondenseTimeInSeconds: z.number().optional(),
/**
* Per-model-profile overrides for idleCondenseContextPercent. A value of -1
* inherits the global idleCondenseContextPercent; values outside
* [MIN_CONDENSE_THRESHOLD, MAX_CONDENSE_THRESHOLD] fall back to global.
*/
profileIdleThresholds: z.record(z.string(), z.number()).optional(),
/**
* Per-model-profile overrides for idleCondenseTimeInSeconds. A value of -1
* inherits the global idleCondenseTimeInSeconds; negative values other than
* -1 fall back to global.
*/
profileIdleCondenseTimes: z.record(z.string(), z.number()).optional(),

/**
* Whether to include current time in the environment details
Expand Down
6 changes: 6 additions & 0 deletions packages/types/src/vscode-extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,8 @@ export type ExtensionState = Pick<
| "codebaseIndexConfig"
| "codebaseIndexModels"
| "profileThresholds"
| "profileIdleThresholds"
| "profileIdleCondenseTimes"
| "includeDiagnosticMessages"
| "maxDiagnosticMessages"
| "imageGenerationProvider"
Expand Down Expand Up @@ -412,6 +414,9 @@ export type ExtensionState = Pick<

autoCondenseContext: boolean
autoCondenseContextPercent: number
idleCondenseContext: boolean
idleCondenseContextPercent: number
idleCondenseTimeInSeconds: number
marketplaceItems?: MarketplaceItem[]
// eslint-disable-next-line @typescript-eslint/no-explicit-any
marketplaceInstalledMetadata?: { project: Record<string, any>; global: Record<string, any> }
Expand Down Expand Up @@ -629,6 +634,7 @@ export interface WebviewMessage {
| "zooCodeSignOut"
| "switchOrganization"
| "condenseTaskContextRequest"
| "idleCondenseReset"
| "requestIndexingStatus"
| "startIndexing"
| "stopIndexing"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
truncateConversation,
manageContext,
willManageContext,
computeEffectiveCondenseThreshold,
} from "../index"

// Create a mock ApiHandler for testing
Expand Down Expand Up @@ -1247,6 +1248,96 @@ describe("Context Management", () => {
})
})

/**
* Tests for the computeEffectiveCondenseThreshold helper
*/
describe("computeEffectiveCondenseThreshold", () => {
it("returns the profile threshold when it is within the valid range", () => {
expect(
computeEffectiveCondenseThreshold({
globalPercent: 80,
profileThresholds: { architect: 40 },
currentProfileId: "architect",
}),
).toBe(40)
})

it("returns the global percent when the profile threshold is -1 (inheritance)", () => {
expect(
computeEffectiveCondenseThreshold({
globalPercent: 80,
profileThresholds: { architect: -1 },
currentProfileId: "architect",
}),
).toBe(80)
})

it("returns the global percent when the profile has no specific threshold", () => {
expect(
computeEffectiveCondenseThreshold({
globalPercent: 80,
profileThresholds: { code: 40 },
currentProfileId: "architect",
}),
).toBe(80)
})

it("warns and returns the global percent when the profile threshold is below the minimum", () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
try {
expect(
computeEffectiveCondenseThreshold({
globalPercent: 80,
profileThresholds: { architect: 1 },
currentProfileId: "architect",
}),
).toBe(80)
expect(warnSpy).toHaveBeenCalledWith(
'Invalid profile threshold 1 for profile "architect". Using global default of 80%',
)
} finally {
warnSpy.mockRestore()
}
})

it("warns and returns the global percent when the profile threshold is above the maximum", () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
try {
expect(
computeEffectiveCondenseThreshold({
globalPercent: 80,
profileThresholds: { architect: 150 },
currentProfileId: "architect",
}),
).toBe(80)
expect(warnSpy).toHaveBeenCalledWith(
'Invalid profile threshold 150 for profile "architect". Using global default of 80%',
)
} finally {
warnSpy.mockRestore()
}
})

it("uses a custom label in the warning message", () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
try {
expect(
computeEffectiveCondenseThreshold({
globalPercent: 180,
profileThresholds: { architect: -5 },
currentProfileId: "architect",
label: "idle time",
}),
).toBe(180)
expect(warnSpy).toHaveBeenCalledWith(
'Invalid profile idle time -5 for profile "architect". Using global default of 180%',
)
} finally {
warnSpy.mockRestore()
}
})
})

/**
* Tests for the getMaxTokens function (private but tested through manageContext)
*/
Expand Down
36 changes: 35 additions & 1 deletion src/core/context-management/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export async function estimateTokenCount(
* (window minus reserved output); an unknown/unlimited reserve (maxTokens -1) falls back to the
* full window. Shared by `willManageContext` and `manageContext` so the two stay in lockstep.
*/
function computeContextPercent({
export function computeContextPercent({
prevContextTokens,
contextWindow,
maxTokens,
Expand Down Expand Up @@ -158,6 +158,40 @@ export function truncateConversation(messages: ApiMessage[], fracToRemove: numbe
}
}

/**
* Resolves the effective condense threshold percent for the current model
* profile, mirroring the semantics used by `willManageContext`/`manageContext`:
* a profile value of -1 inherits the global percent, a value within
* [MIN_CONDENSE_THRESHOLD, MAX_CONDENSE_THRESHOLD] overrides it, and any other
* value falls back to the global percent with a warning.
*/
export function computeEffectiveCondenseThreshold({
globalPercent,
profileThresholds,
currentProfileId,
label = "threshold",
}: {
globalPercent: number
profileThresholds: Record<string, number>
currentProfileId: string
label?: string
}): number {
const profileThreshold = profileThresholds[currentProfileId]
if (profileThreshold === undefined) {
return globalPercent
}
if (profileThreshold === -1) {
return globalPercent
}
if (profileThreshold >= MIN_CONDENSE_THRESHOLD && profileThreshold <= MAX_CONDENSE_THRESHOLD) {
return profileThreshold
}
console.warn(
`Invalid profile ${label} ${profileThreshold} for profile "${currentProfileId}". Using global default of ${globalPercent}%`,
)
return globalPercent
}

/**
* Options for checking if context management will likely run.
* A subset of ContextManagementOptions with only the fields needed for threshold calculation.
Expand Down
Loading
Loading