Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/session-rating-survey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": minor
---

Add an occasional session rating prompt above the input box.
1 change: 1 addition & 0 deletions apps/kimi-code/src/cli/run-shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ export async function runShell(
migrationPlan,
migrateOnly: runOptions.migrateOnly,
engineV2,
telemetryDisabled: config.telemetry === false,
});

initializeCliTelemetry({
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/src/constant/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export const KIMI_CODE_UPDATE_REEXEC_ENV = 'KIMI_CODE_UPDATE_REEXEC';
export const KIMI_CODE_INPUT_HISTORY_DIR_NAME = 'user-history';
export const KIMI_CODE_BANNER_DIR_NAME = 'banner';
export const KIMI_CODE_BANNER_STATE_FILE_NAME = 'state.json';
export const KIMI_CODE_SURVEY_STATE_FILE_NAME = 'feedback-survey-state.json';

// Managed Kimi auth provider key shared with OAuth/SDK config.
export const DEFAULT_OAUTH_PROVIDER_NAME = 'managed:kimi-code';
Expand Down
59 changes: 59 additions & 0 deletions apps/kimi-code/src/tui/commands/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { modelDisplayName, segmentsFor } from '../components/dialogs/model-selec
import { TabbedModelSelectorComponent } from '../components/dialogs/tabbed-model-selector';
import { PermissionSelectorComponent } from '../components/dialogs/permission-selector';
import { SettingsSelectorComponent, type SettingsSelection } from '../components/dialogs/settings-selector';
import { SurveyPreferenceSelectorComponent } from '../components/dialogs/survey-preference-selector';
import { ThemeSelectorComponent } from '../components/dialogs/theme-selector';
import { UpdatePreferenceSelectorComponent } from '../components/dialogs/update-preference-selector';
import { DEFAULT_TUI_CONFIG, saveTuiConfig, type TuiConfig } from '../config';
Expand Down Expand Up @@ -60,6 +61,8 @@ export function currentTuiConfig(host: Pick<SlashCommandHost, 'state'>): TuiConf
disablePasteBurst: host.state.appState.disablePasteBurst ?? DEFAULT_TUI_CONFIG.disablePasteBurst,
renderLatex: host.state.appState.renderLatex ?? DEFAULT_TUI_CONFIG.renderLatex ?? true,
cacheExpiryHint: host.state.appState.cacheExpiryHint ?? DEFAULT_TUI_CONFIG.cacheExpiryHint,
disableFeedbackSurvey:
host.state.appState.disableFeedbackSurvey ?? DEFAULT_TUI_CONFIG.disableFeedbackSurvey,
notifications: host.state.appState.notifications,
upgrade: host.state.appState.upgrade,
statusLine: host.state.appState.statusLine ?? DEFAULT_TUI_CONFIG.statusLine,
Expand Down Expand Up @@ -916,6 +919,61 @@ async function applyPermissionChoice(host: SlashCommandHost, mode: PermissionMod
}
}

export function showSurveyPreferencePicker(host: SlashCommandHost): void {
host.mountEditorReplacement(
new SurveyPreferenceSelectorComponent({
currentValue: host.state.appState.disableFeedbackSurvey !== true,
onSelect: (value) => {
host.restoreEditor();
void applySurveyPreferenceChoice(host, value);
},
onCancel: () => {
host.restoreEditor();
},
}),
);
}

type SurveyPreferenceHost = {
readonly state: {
readonly appState: Pick<
SlashCommandHost['state']['appState'],
'theme' | 'editorCommand' | 'notifications' | 'upgrade' | 'disableFeedbackSurvey'
>;
};
setAppState(
patch: Pick<SlashCommandHost['state']['appState'], 'disableFeedbackSurvey'>,
): void;
showStatus(msg: string, color?: string): void;
};

export async function applySurveyPreferenceChoice(
host: SurveyPreferenceHost,
enabled: boolean,
): Promise<void> {
const disableFeedbackSurvey = !enabled;
if (disableFeedbackSurvey === (host.state.appState.disableFeedbackSurvey === true)) {
host.showStatus(`Feedback survey already ${enabled ? 'enabled' : 'disabled'}.`);
return;
}

try {
await saveTuiConfig({
...currentTuiConfig(host as unknown as SlashCommandHost),
disableFeedbackSurvey,
});
} catch (error) {
host.showStatus(
`Failed to save session rating setting: ${formatErrorMessage(error)}`,
'error',
);
return;
}

host.setAppState({ disableFeedbackSurvey });
host.showStatus(`Feedback survey ${enabled ? 'enabled' : 'disabled'}.`);
}

export function showSettingsSelector(host: SlashCommandHost): void {
host.mountEditorReplacement(
new SettingsSelectorComponent({
Expand All @@ -936,6 +994,7 @@ function handleSettingsSelection(host: SlashCommandHost, value: SettingsSelectio
case 'permission': showPermissionPicker(host); return;
case 'theme': showThemePicker(host); return;
case 'editor': showEditorPicker(host); return;
case 'survey': showSurveyPreferencePicker(host); return;
case 'experiments': void showExperimentsPanel(host); return;
case 'upgrade': showUpdatePreferencePicker(host); return;
case 'usage': void showUsage(host); return;
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/src/tui/commands/reload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export async function applyReloadedTuiConfig(
disablePasteBurst: config.disablePasteBurst,
renderLatex: config.renderLatex,
cacheExpiryHint: config.cacheExpiryHint,
disableFeedbackSurvey: config.disableFeedbackSurvey,
notifications: config.notifications,
upgrade: config.upgrade,
statusLine: config.statusLine,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type SettingsSelection =
| 'theme'
| 'editor'
| 'permission'
| 'survey'
| 'experiments'
| 'upgrade'
| 'usage';
Expand All @@ -30,6 +31,11 @@ const SETTINGS_OPTIONS: readonly ChoiceOption[] = [
label: 'Editor',
description: 'Set the external editor command.',
},
{
value: 'survey',
label: 'Feedback survey',
description: 'Turn the occasional session rating prompt on or off.',
},
{
value: 'experiments',
label: 'Experiments',
Expand All @@ -53,6 +59,7 @@ function isSettingsSelection(value: string): value is SettingsSelection {
value === 'theme' ||
value === 'editor' ||
value === 'permission' ||
value === 'survey' ||
value === 'experiments' ||
value === 'upgrade' ||
value === 'usage'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ChoicePickerComponent, type ChoiceOption } from './choice-picker';

const SURVEY_PREFERENCE_OPTIONS: readonly ChoiceOption[] = [
{
value: 'on',
label: 'On',
description: 'Show the occasional rating prompt above the editor.',
},
{
value: 'off',
label: 'Off',
description: 'Never show the rating prompt.',
},
];

export interface SurveyPreferenceSelectorOptions {
readonly currentValue: boolean;
readonly onSelect: (value: boolean) => void;
readonly onCancel: () => void;
}

export class SurveyPreferenceSelectorComponent extends ChoicePickerComponent {
constructor(opts: SurveyPreferenceSelectorOptions) {
super({
title: 'Feedback survey',
options: [...SURVEY_PREFERENCE_OPTIONS],
currentValue: opts.currentValue ? 'on' : 'off',
onSelect: (value) => {
opts.onSelect(value === 'on');
},
onCancel: opts.onCancel,
});
}
}
7 changes: 6 additions & 1 deletion apps/kimi-code/src/tui/components/editor/custom-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ export class CustomEditor extends Editor {
* double-Esc so only two consecutive Escape presses trigger the shortcut.
*/
public onNonEscapeInput?: () => void;
public onPreInput?: (data: string) => boolean;
public onCtrlD?: () => void;
public onCtrlC?: () => void;
public onToggleToolExpand?: () => void;
Expand Down Expand Up @@ -256,7 +257,7 @@ export class CustomEditor extends Editor {
return false;
}

private hasAutocompleteActivity(): boolean {
public hasAutocompleteActivity(): boolean {
const autocomplete = this as unknown as AutocompleteInternals;
return (
this.isShowingAutocomplete() ||
Expand Down Expand Up @@ -382,6 +383,10 @@ export class CustomEditor extends Editor {
this.onNonEscapeInput?.();
}

if (this.onPreInput?.(normalized) === true) {
return;
}

// When a paste marker was just expanded, discard the trailing bracketed
// paste data that the terminal sends alongside the Ctrl-V keystroke.
if (this.consumingPaste) {
Expand Down
96 changes: 96 additions & 0 deletions apps/kimi-code/src/tui/components/panes/survey-panel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import type { Component } from '@moonshot-ai/pi-tui';
import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from '@moonshot-ai/pi-tui';

import {
SURVEY_MIN_OPTIONS_WIDTH,
SURVEY_OPTION_GAP,
SURVEY_OPTION_LABELS,
SURVEY_QUESTION,
} from '../../constant/survey';

import { currentTheme } from '../../theme';
import type { SurveyResponse } from '../../utils/survey-policy';

export type SurveyPanelPhase = 'open' | 'pending' | 'thanks';

export interface SurveyPanelView {
phase: SurveyPanelPhase;
response?: Exclude<SurveyResponse, 'dismissed'>;
hoverIndex?: number;
}

const DOT = '●';
const DOT_PREFIX_WIDTH = 2;
const OPTION_INDENT = ' ';

const RESPONSE_LABELS: Record<Exclude<SurveyResponse, 'dismissed'>, string> = {
bad: 'Bad',
fine: 'Fine',
good: 'Good',
};
const THANKS = 'Thanks for your feedback!';

export class SurveyPanelComponent implements Component {
constructor(private readonly view: SurveyPanelView) {}

invalidate(): void {}

render(width: number): string[] {
if (width < 1) return [''];
switch (this.view.phase) {
case 'open':
return this.renderOpen(width);
case 'pending': {
const label =
this.view.response === undefined ? '' : RESPONSE_LABELS[this.view.response];
return this.renderStatusLine(width, currentTheme.fg('textDim', `Feedback: ${label} · [escape: undo]`));
}
case 'thanks':
return this.renderStatusLine(width, currentTheme.fg('success', THANKS));
}
}

private renderOpen(width: number): string[] {
const title = wrapTextWithAnsi(SURVEY_QUESTION, Math.max(1, width - DOT_PREFIX_WIDTH)).map(
(line, index) =>
(index === 0 ? this.dotPrefix() : ' '.repeat(DOT_PREFIX_WIDTH)) +
currentTheme.boldFg('textStrong', line),
);
const optionsLine = OPTION_INDENT + this.styledOptions();
if (visibleWidth(optionsLine) <= width) {
return [...title, optionsLine];
}
if (width >= SURVEY_MIN_OPTIONS_WIDTH) {
return [
...title,
...this.styledOptionsPerLine().map((option) => OPTION_INDENT + option),
];
}
return title;
}

private renderStatusLine(width: number, styledText: string): string[] {
return [truncateToWidth(this.dotPrefix() + styledText, width)];
}

private dotPrefix(): string {
return currentTheme.fg('accent', DOT) + ' ';
}

private styledOptions(): string {
return SURVEY_OPTION_LABELS.map((label, index) => this.styleOption(label, index)).join(
' '.repeat(SURVEY_OPTION_GAP),
);
}

private styledOptionsPerLine(): string[] {
return SURVEY_OPTION_LABELS.map((label, index) => this.styleOption(label, index));
}

private styleOption(label: string, index: number): string {
if (this.view.hoverIndex === index) {
return currentTheme.bg('border', currentTheme.boldFg('textStrong', label));
}
return currentTheme.fg('text', label);
}
}
6 changes: 6 additions & 0 deletions apps/kimi-code/src/tui/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export const TuiConfigFileSchema = z.object({
render_latex: z.boolean().optional(),
disable_paste_burst: z.boolean().optional(),
cache_expiry_hint: z.boolean().optional(),
disable_feedback_survey: z.boolean().optional(),
editor: z
.object({
command: z.string().optional(),
Expand Down Expand Up @@ -84,6 +85,7 @@ export const TuiConfigSchema = z.object({
/** Present in every normalized config; optional only so hand-built test
* fixtures from before this field existed still typecheck. */
cacheExpiryHint: z.boolean().optional(),
disableFeedbackSurvey: z.boolean().optional(),
editorCommand: z.string().nullable(),
notifications: NotificationsConfigSchema,
upgrade: UpgradePreferencesSchema,
Expand Down Expand Up @@ -111,6 +113,7 @@ export const DEFAULT_TUI_CONFIG: TuiConfig = TuiConfigSchema.parse({
renderLatex: true,
disablePasteBurst: false,
cacheExpiryHint: true,
disableFeedbackSurvey: false,
editorCommand: null,
notifications: DEFAULT_NOTIFICATIONS_CONFIG,
upgrade: DEFAULT_UPGRADE_PREFERENCES,
Expand Down Expand Up @@ -198,6 +201,8 @@ export function normalizeTuiConfig(
renderLatex: config.render_latex ?? DEFAULT_TUI_CONFIG.renderLatex,
disablePasteBurst: config.disable_paste_burst ?? DEFAULT_TUI_CONFIG.disablePasteBurst,
cacheExpiryHint: config.cache_expiry_hint ?? DEFAULT_TUI_CONFIG.cacheExpiryHint,
disableFeedbackSurvey:
config.disable_feedback_survey ?? DEFAULT_TUI_CONFIG.disableFeedbackSurvey,
editorCommand: command === undefined || command.length === 0 ? null : command,
notifications: {
enabled: config.notifications?.enabled ?? DEFAULT_NOTIFICATIONS_CONFIG.enabled,
Expand Down Expand Up @@ -248,6 +253,7 @@ theme = "${escapeTomlBasicString(config.theme)}" # "auto" | "dark" | "light" | c
render_latex = ${String(config.renderLatex !== false)} # false keeps LaTeX math in assistant messages as raw source
disable_paste_burst = ${String(config.disablePasteBurst)} # true disables non-bracketed paste-burst fallback
cache_expiry_hint = ${String(config.cacheExpiryHint !== false)} # false disables the "cache expired" dialog on resume / idle submit
disable_feedback_survey = ${String(config.disableFeedbackSurvey === true)} # true hides the occasional session rating prompt

[editor]
command = "${escapeTomlBasicString(config.editorCommand ?? '')}" # Empty uses $VISUAL / $EDITOR
Expand Down
Loading
Loading