Skip to content

Commit eec9bbd

Browse files
author
grapedge
authored
feat(kimi-code): support per-model survey popup overrides (#4033)
1 parent 0f052fe commit eec9bbd

8 files changed

Lines changed: 608 additions & 58 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@moonshot-ai/kimi-code": patch
3+
---
4+
5+
Feedback surveys now appear less often.

‎apps/kimi-code/src/tui/controllers/survey-controller.ts‎

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import {
88
getSurveyPopupConfig,
99
peekSurveyPopupConfig,
1010
peekSurveyPopupConfigFresh,
11+
resolveSurveyPopupConfig,
1112
type SurveyPopupConfig,
13+
type SurveyPopupPayload,
1214
} from '#/utils/survey-popup-config';
1315
import { readSurveyLastShownTime, writeSurveyLastShownTime } from '#/utils/survey-state-store';
1416
import { currentKimiRegion } from '#/utils/region';
@@ -94,7 +96,7 @@ function joinModels(models: ReadonlySet<string>): string | undefined {
9496
}
9597

9698
export interface SurveyControllerDeps {
97-
readonly config?: () => SurveyPopupConfig;
99+
readonly config?: () => SurveyPopupPayload;
98100
readonly monotonicNow?: () => number;
99101
readonly wallNow?: () => number;
100102
readonly random?: () => number;
@@ -145,7 +147,8 @@ export class SurveyController {
145147
private userTurnsAtLastShown: number | undefined;
146148
private appearanceCount = 0;
147149
private globalLastShownAt: number | undefined;
148-
private longContextRollConsumed = false;
150+
private readonly longContextRollConsumedModels = new Set<string | undefined>();
151+
private longContextShownThisMount = false;
149152
private generation = 0;
150153
private idleSince: number | undefined;
151154
private openedAt = 0;
@@ -196,7 +199,8 @@ export class SurveyController {
196199
this.lastShownAt = undefined;
197200
this.userTurnsAtLastShown = undefined;
198201
this.appearanceCount = 0;
199-
this.longContextRollConsumed = false;
202+
this.longContextRollConsumedModels.clear();
203+
this.longContextShownThisMount = false;
200204
this.idleSince = undefined;
201205
this.stickySample = undefined;
202206
this.toolCallCount = 0;
@@ -444,6 +448,13 @@ export class SurveyController {
444448
return false;
445449
}
446450

451+
private currentConfig(): SurveyPopupConfig {
452+
return resolveSurveyPopupConfig(
453+
(this.deps.config ?? defaultDeps.config)(),
454+
resolveKfcModelId(this.host.state.appState),
455+
);
456+
}
457+
447458
private evaluate(): void {
448459
if (this.machine.phase !== 'closed') return;
449460
if (!this.configReady) return;
@@ -463,9 +474,12 @@ export class SurveyController {
463474
) {
464475
return;
465476
}
466-
const config = (this.deps.config ?? defaultDeps.config)();
467-
const verdict = evaluateSurveyGate({ ...this.gateInputs(), config });
468-
if (verdict.longContextRollConsumed === true) this.longContextRollConsumed = true;
477+
const config = this.currentConfig();
478+
const inputs = this.gateInputs();
479+
const verdict = evaluateSurveyGate({ ...inputs, config });
480+
if (verdict.longContextRollConsumed === true) {
481+
this.longContextRollConsumedModels.add(inputs.longContext.kfcModelId);
482+
}
469483
if (!verdict.show) return;
470484
this.open(verdict.survey, config);
471485
}
@@ -508,7 +522,12 @@ export class SurveyController {
508522
...shared,
509523
cumulativeTokens: appState.cumulativeTokens ?? 0,
510524
virtualContextTokens: appState.contextTokens,
511-
mountRollConsumed: this.longContextRollConsumed,
525+
mountRollConsumed: this.longContextRollConsumedModels.has(shared.kfcModelId),
526+
mountSurveyShown: this.longContextShownThisMount,
527+
msSinceGlobalLastShown:
528+
this.globalLastShownAt === undefined
529+
? undefined
530+
: this.wallNow() - this.globalLastShownAt,
512531
drawMountRoll: () => (this.deps.random ?? defaultDeps.random)(),
513532
},
514533
};
@@ -564,7 +583,7 @@ export class SurveyController {
564583
this.openedEditorText = this.host.state.editor.getText();
565584
this.lastShownAt = shownAt;
566585
this.userTurnsAtLastShown = this.userTurnCount;
567-
if (survey !== 'session') return;
586+
if (survey === 'long_context') this.longContextShownThisMount = true;
568587
this.globalLastShownAt = this.wallNow();
569588
try {
570589
(this.deps.writeGlobalLastShown ?? defaultDeps.writeGlobalLastShown)(
@@ -598,7 +617,7 @@ export class SurveyController {
598617
response: effect.response,
599618
},
600619
this.appearanceSnapshot?.fields ?? this.environmentFields(),
601-
this.appearanceConfig ?? (this.deps.config ?? defaultDeps.config)(),
620+
this.appearanceConfig ?? this.currentConfig(),
602621
);
603622
const sessionId = this.appearanceSnapshot?.sessionId ?? '';
604623
if (sessionId.length > 0) {

‎apps/kimi-code/src/tui/utils/survey-policy.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export type SurveyKind = 'session' | 'long_context';
1111

1212
export type SurveyGateSkipReason =
1313
| 'mount-roll-consumed'
14+
| 'mount-survey-shown'
1415
| 'survey-active'
1516
| 'turn-in-progress'
1617
| 'idle-too-short'
@@ -60,6 +61,8 @@ export interface LongContextArmGateInput extends SharedArmGateInput {
6061
readonly cumulativeTokens: number;
6162
readonly virtualContextTokens: number;
6263
readonly mountRollConsumed: boolean;
64+
readonly mountSurveyShown: boolean;
65+
readonly msSinceGlobalLastShown: number | undefined;
6366
readonly drawMountRoll: () => number;
6467
}
6568

@@ -141,6 +144,7 @@ function evaluateSessionArm(input: SurveyGateInput): SurveyGateVerdict {
141144

142145
export function evaluateLongContextArm(input: SurveyGateInput): SurveyGateVerdict {
143146
const { longContext, config } = input;
147+
if (longContext.mountSurveyShown) return { show: false, reason: 'mount-survey-shown' };
144148
if (longContext.mountRollConsumed) return { show: false, reason: 'mount-roll-consumed' };
145149
if (longContext.phase !== 'closed') return { show: false, reason: 'survey-active' };
146150
if (longContext.turnInProgress) return { show: false, reason: 'turn-in-progress' };
@@ -179,6 +183,12 @@ export function evaluateLongContextArm(input: SurveyGateInput): SurveyGateVerdic
179183
if (counter < config.long_context_survey_threshold) {
180184
return { show: false, reason: 'below-threshold' };
181185
}
186+
if (
187+
longContext.msSinceGlobalLastShown !== undefined &&
188+
longContext.msSinceGlobalLastShown < config.min_time_between_global_feedback_ms
189+
) {
190+
return { show: false, reason: 'global-cooldown' };
191+
}
182192
if (longContext.drawMountRoll() >= config.long_context_probability) {
183193
return { show: false, reason: 'sampled-out', longContextRollConsumed: true };
184194
}

‎apps/kimi-code/src/utils/survey-popup-config.ts‎

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,17 @@ export const DEFAULT_SURVEY_POPUP_CONFIG: SurveyPopupConfig = {
4242
long_context_trigger_mode: 'virtual_context',
4343
};
4444

45+
export type SurveyPopupOverride = Partial<Omit<SurveyPopupConfig, 'on_for_models'>>;
46+
47+
export interface SurveyPopupPayload extends SurveyPopupConfig {
48+
model_overrides: Record<string, SurveyPopupOverride>;
49+
}
50+
51+
export const DEFAULT_SURVEY_POPUP_PAYLOAD: SurveyPopupPayload = {
52+
...DEFAULT_SURVEY_POPUP_CONFIG,
53+
model_overrides: {},
54+
};
55+
4556
const FIELD_SCHEMAS = {
4657
probability: z.number().min(0).max(1),
4758
on_for_models: z.array(z.string()),
@@ -55,7 +66,19 @@ const FIELD_SCHEMAS = {
5566
long_context_trigger_mode: z.enum(['cumulative', 'virtual_context']),
5667
} satisfies Record<keyof SurveyPopupConfig, z.ZodType>;
5768

58-
const surveyPopupConfigSchema = z.unknown().transform((raw): Partial<SurveyPopupConfig> => {
69+
const surveyPopupOverrideSchema = z.object(FIELD_SCHEMAS).omit({ on_for_models: true }).partial();
70+
71+
function parseModelOverrides(raw: unknown): Record<string, SurveyPopupOverride> {
72+
if (typeof raw !== 'object' || raw === null) return {};
73+
const overrides: Record<string, SurveyPopupOverride> = {};
74+
for (const [model, entry] of Object.entries(raw)) {
75+
const parsed = surveyPopupOverrideSchema.safeParse(entry);
76+
if (parsed.success) overrides[model] = parsed.data;
77+
}
78+
return overrides;
79+
}
80+
81+
const surveyPopupConfigSchema = z.unknown().transform((raw): Partial<SurveyPopupPayload> => {
5982
if (typeof raw !== 'object' || raw === null) return {};
6083
const record = raw as Record<string, unknown>;
6184
const partial: Record<string, unknown> = {};
@@ -65,23 +88,35 @@ const surveyPopupConfigSchema = z.unknown().transform((raw): Partial<SurveyPopup
6588
const parsed = schema.safeParse(value);
6689
if (parsed.success) partial[key] = parsed.data;
6790
}
68-
return partial as Partial<SurveyPopupConfig>;
91+
if (record['model_overrides'] !== undefined) {
92+
partial['model_overrides'] = parseModelOverrides(record['model_overrides']);
93+
}
94+
return partial as Partial<SurveyPopupPayload>;
6995
});
7096

71-
function withDefaults(partial: Partial<SurveyPopupConfig> | undefined): SurveyPopupConfig {
72-
return { ...DEFAULT_SURVEY_POPUP_CONFIG, ...partial };
97+
function withDefaults(partial: Partial<SurveyPopupPayload> | undefined): SurveyPopupPayload {
98+
return { ...DEFAULT_SURVEY_POPUP_PAYLOAD, ...partial };
7399
}
74100

75101
export async function getSurveyPopupConfig(
76102
options: ClientConfigFetchOptions = {},
77-
): Promise<SurveyPopupConfig> {
103+
): Promise<SurveyPopupPayload> {
78104
return withDefaults(await getClientConfig(CONFIG_NAME, surveyPopupConfigSchema, options));
79105
}
80106

81-
export function peekSurveyPopupConfig(now?: number): SurveyPopupConfig {
107+
export function peekSurveyPopupConfig(now?: number): SurveyPopupPayload {
82108
return withDefaults(peekClientConfig(CONFIG_NAME, surveyPopupConfigSchema, now));
83109
}
84110

111+
export function resolveSurveyPopupConfig(
112+
payload: SurveyPopupPayload,
113+
kfcModelId: string | undefined,
114+
): SurveyPopupConfig {
115+
const { model_overrides: modelOverrides, ...base } = payload;
116+
const override = kfcModelId === undefined ? undefined : modelOverrides[kfcModelId];
117+
return override === undefined ? base : { ...base, ...override };
118+
}
119+
85120
export function peekSurveyPopupConfigFresh(now?: number): boolean {
86121
return peekClientConfig(CONFIG_NAME, surveyPopupConfigSchema, now) !== undefined;
87122
}

0 commit comments

Comments
 (0)