Skip to content
Open
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
1 change: 1 addition & 0 deletions packages/cli/src/tests/exec-runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ function createSettings(
permissions,
enabledSkills: {},
statusline: { enabled: false, refreshMs: 1000, separator: " | ", providers: [] },
profiles: {},
};
}

Expand Down
61 changes: 49 additions & 12 deletions packages/cli/src/ui/components/ModelsDropdown/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ export const MODEL_COMMAND_THINKING_OPTIONS: ThinkingModeOption[] = [
{ label: "No thinking", thinkingEnabled: false },
];

export type ModelProfileOption = {
name: string;
};

function getThinkingOptionIndex(config: Pick<ModelConfigSelection, "thinkingEnabled" | "reasoningEffort">): number {
const index = MODEL_COMMAND_THINKING_OPTIONS.findIndex((option) => {
if (!config.thinkingEnabled) {
Expand All @@ -33,17 +37,21 @@ type Props = {
open: boolean;
modelConfig: ModelConfigSelection;
width: number;
profiles?: ModelProfileOption[];
onClose: () => void;
onModelConfigChange: (selection: ModelConfigSelection) => string | Promise<string>;
onProfileSelect?: (profileName: string) => string | Promise<string>;
onStatusMessage?: (message: string | null) => void;
};

const ModelsDropdown: React.FC<Props> = ({
open,
modelConfig,
width,
profiles,
onClose,
onModelConfigChange,
onProfileSelect,
onStatusMessage,
}) => {
const [step, setStep] = useState<ModelStep | null>(null);
Expand All @@ -53,29 +61,47 @@ const ModelsDropdown: React.FC<Props> = ({
// Initialize state when opened
useEffect(() => {
if (open) {
const profileCount = profiles?.length ?? 0;
const currentIndex = MODEL_COMMAND_MODELS.findIndex((m) => m === modelConfig.model);
setPendingModel(null);
setStep("model");
setActiveIndex(currentIndex >= 0 ? currentIndex : 0);
setActiveIndex(currentIndex >= 0 ? profileCount + currentIndex : 0);
} else {
setStep(null);
}
}, [open, modelConfig.model]);
}, [open, modelConfig.model, profiles]);

// Validate activeIndex bounds
useEffect(() => {
if (!step) {
return;
}
const optionCount = step === "model" ? MODEL_COMMAND_MODELS.length : MODEL_COMMAND_THINKING_OPTIONS.length;
const optionCount =
step === "model" ? (profiles?.length ?? 0) + MODEL_COMMAND_MODELS.length : MODEL_COMMAND_THINKING_OPTIONS.length;
if (activeIndex >= optionCount) {
setActiveIndex(Math.max(0, optionCount - 1));
}
}, [activeIndex, step]);
}, [activeIndex, step, profiles]);

function selectItem(): void {
if (step === "model") {
const model = MODEL_COMMAND_MODELS[activeIndex] ?? modelConfig.model;
const profileCount = profiles?.length ?? 0;
if (activeIndex < profileCount && profiles?.[activeIndex] && onProfileSelect) {
const profile = profiles[activeIndex];
onClose();
Promise.resolve(onProfileSelect(profile.name))
.then((message) => {
if (message) {
onStatusMessage?.(message);
}
})
.catch((error) => {
const msg = error instanceof Error ? error.message : String(error);
onStatusMessage?.(`Failed to apply profile: ${msg}`);
});
return;
}
const model = MODEL_COMMAND_MODELS[activeIndex - profileCount] ?? modelConfig.model;
setPendingModel(model);
setStep("thinking");
setActiveIndex(getThinkingOptionIndex(modelConfig));
Expand Down Expand Up @@ -107,7 +133,10 @@ const ModelsDropdown: React.FC<Props> = ({
return;
}

const optionCount = step === "model" ? MODEL_COMMAND_MODELS.length : MODEL_COMMAND_THINKING_OPTIONS.length;
const optionCount =
step === "model"
? (profiles?.length ?? 0) + MODEL_COMMAND_MODELS.length
: MODEL_COMMAND_THINKING_OPTIONS.length;

if (key.upArrow) {
setActiveIndex((idx) => (idx - 1 + optionCount) % optionCount);
Expand Down Expand Up @@ -135,12 +164,20 @@ const ModelsDropdown: React.FC<Props> = ({

const items =
step === "model"
? MODEL_COMMAND_MODELS.map((model) => ({
key: model,
label: model,
description: model === modelConfig.model ? "current model" : "",
selected: model === (pendingModel ?? modelConfig.model),
}))
? [
...(profiles ?? []).map((profile) => ({
key: `profile:${profile.name}`,
label: `⚡ ${profile.name}`,
description: "apply saved profile",
selected: false,
})),
...MODEL_COMMAND_MODELS.map((model) => ({
key: model,
label: model,
description: model === modelConfig.model ? "current model" : "",
selected: model === (pendingModel ?? modelConfig.model),
})),
]
: MODEL_COMMAND_THINKING_OPTIONS.map((option, i) => ({
key: option.label,
label: option.label,
Expand Down
56 changes: 55 additions & 1 deletion packages/cli/src/ui/views/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { MessageView, RawModeExitPrompt } from "../components";
import { SessionList } from "./SessionList";
import { type UndoRestoreMode, UndoSelector } from "./UndoSelector";
import { buildLoadingText } from "../core/loading-text";
import type { ModelProfileOption } from "../components/ModelsDropdown";
import { findExpandedThinkingId } from "../core/thinking-state";
import { WelcomeScreen } from "./WelcomeScreen";
import { AskUserQuestionPrompt } from "./AskUserQuestionPrompt";
Expand All @@ -32,7 +33,12 @@ import {
isCurrentSessionEmpty,
renderRawModeMessages,
} from "../utils";
import { resolveCurrentSettings, writeModelConfigSelection } from "@vegamo/deepcode-core";
import {
getModelProfiles,
resolveCurrentSettings,
writeModelConfigSelection,
writeProfileSelection,
} from "@vegamo/deepcode-core";
import { useStatusLine } from "../hooks";
import type { SessionInfo } from "../statusline";
import { isCollapsedThinking } from "../core/thinking-state";
Expand Down Expand Up @@ -131,6 +137,10 @@ function App({ projectRoot, initialPrompt, resumeSessionId, forkSessionId, onRes
const [showWelcome, setShowWelcome] = useState(true);
const [welcomeNonce, setWelcomeNonce] = useState(0);
const [resolvedSettings, setResolvedSettings] = useState(() => resolveCurrentSettings(projectRoot));
const modelProfiles = useMemo<ModelProfileOption[]>(
() => Object.keys(getModelProfiles(resolvedSettings)).map((name) => ({ name })),
[resolvedSettings]
);
const [nowTick, setNowTick] = useState(0);
const [mcpStatuses, setMcpStatuses] = useState<ReturnType<typeof sessionManager.getMcpStatus>>([]);
const [showProcessStdout, setShowProcessStdout] = useState(false);
Expand Down Expand Up @@ -539,6 +549,48 @@ function App({ projectRoot, initialPrompt, resumeSessionId, forkSessionId, onRes
[handlePrompt]
);

const handleProfileSelect = useCallback(
(profileName: string): string => {
const { changed, profile } = writeProfileSelection(profileName, projectRoot);
const next = resolveCurrentSettings(projectRoot);
setResolvedSettings(next);
if (!changed || !profile) {
return `Profile "${profileName}" not found in settings.json`;
}

const activeSessionId = sessionManager.getActiveSessionId();
const content = `/model\n└ Applied profile ${profileName} → ${profile.model ?? next.model} (${
next.thinkingEnabled ? next.reasoningEffort : "no thinking"
})`;
if (activeSessionId) {
sessionManager.addSessionSystemMessage(activeSessionId, content, true, { isModelChange: true });
const activeSession = sessionManager.getSession(activeSessionId);
setStatusLine(activeSession ? buildStatusLine(activeSession, next) : "");
} else {
const now = new Date().toISOString();
setMessages((prev) => [
...prev,
{
id: crypto.randomUUID(),
sessionId: "local",
role: "system" as const,
content,
contentParams: null,
messageParams: null,
compacted: false,
visible: true,
createTime: now,
updateTime: now,
meta: { isModelChange: true },
},
]);
}

return `Applied profile "${profileName}" (${profile.model ?? next.model})`;
},
[projectRoot, sessionManager]
);

const handlePlanImplementationChoice = useCallback(
(choice: "implement" | "stay" | "default") => {
const proposedPlan = pendingPlanImplementation;
Expand Down Expand Up @@ -1061,6 +1113,8 @@ function App({ projectRoot, initialPrompt, resumeSessionId, forkSessionId, onRes
promptDraft={promptDraft}
onSubmit={handleSubmit}
onModelConfigChange={handleModelConfigChange}
modelProfiles={modelProfiles}
onProfileSelect={handleProfileSelect}
onRawModeChange={handleRawModeChange}
onInterrupt={handleInterrupt}
onToggleProcessStdout={handleToggleProcessStdout}
Expand Down
7 changes: 7 additions & 0 deletions packages/cli/src/ui/views/PromptInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import {
import SlashCommandMenu, { isSkillSelected } from "./SlashCommandMenu";
import type { ModelConfigSelection, PermissionScope } from "@vegamo/deepcode-core";
import { FileMentionMenu, ModelsDropdown, RawModelDropdown, SkillsDropdown } from "../components";
import type { ModelProfileOption } from "../components/ModelsDropdown";
import type { SessionEntry, SkillInfo } from "@vegamo/deepcode-core";
import type { UserToolPermission } from "@vegamo/deepcode-core";
import type { StatusSegment } from "../statusline";
Expand Down Expand Up @@ -100,6 +101,8 @@ type Props = {
planMode: boolean;
onSubmit: (submission: PromptSubmission) => void;
onModelConfigChange: (selection: ModelConfigSelection) => string | Promise<string>;
modelProfiles?: ModelProfileOption[];
onProfileSelect?: (profileName: string) => string | Promise<string>;
onRawModeChange?: (mode: string) => void;
onPlanModeChange: (enabled: boolean) => void;
onInterrupt: () => void;
Expand Down Expand Up @@ -135,6 +138,8 @@ export const PromptInput = React.memo(function PromptInput({
planMode,
onSubmit,
onModelConfigChange,
modelProfiles,
onProfileSelect,
onInterrupt,
onToggleProcessStdout,
onExitShortcut,
Expand Down Expand Up @@ -851,8 +856,10 @@ export const PromptInput = React.memo(function PromptInput({
open={showModelDropdown}
modelConfig={modelConfig}
width={screenWidth}
profiles={modelProfiles}
onClose={() => setShowModelDropdown(false)}
onModelConfigChange={onModelConfigChange}
onProfileSelect={onProfileSelect}
onStatusMessage={setStatusMessage}
/>
<FileMentionMenu
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export {
getProjectSettingsPath,
getDefaultContextWindow,
getDefaultAutoCompactWindow,
getModelProfiles,
writeProfileSelection,
DEFAULT_MODEL,
DEFAULT_BASE_URL,
} from "./settings";
Expand Down
Loading