{!isHistorical && (
@@ -307,7 +312,12 @@ export function ProgressSection({
)}
-
+
{formatNumber(stoppedGradNorm, 3)}
+ {data.projectName && (
+
+ {data.projectName}
+
+ )}
{data.modelName || "--"}
diff --git a/studio/frontend/src/features/training/api/mappers.ts b/studio/frontend/src/features/training/api/mappers.ts
index d4c800afe4..c2e23dbf3c 100644
--- a/studio/frontend/src/features/training/api/mappers.ts
+++ b/studio/frontend/src/features/training/api/mappers.ts
@@ -73,6 +73,7 @@ export function buildTrainingStartPayload(
return {
model_name: config.selectedModel ?? "",
+ project_name: (config.projectName || "").trim() || null,
training_type: toBackendTrainingType(config.trainingMethod),
hf_token: config.hfToken.trim() || null,
load_in_4bit: (adapterMethod && isQloraMethod) || (isCpt && isFourBitModel),
diff --git a/studio/frontend/src/features/training/hooks/use-training-actions.ts b/studio/frontend/src/features/training/hooks/use-training-actions.ts
index 8c2b3e9e77..2f04656c23 100644
--- a/studio/frontend/src/features/training/hooks/use-training-actions.ts
+++ b/studio/frontend/src/features/training/hooks/use-training-actions.ts
@@ -60,6 +60,7 @@ export function useTrainingActions() {
config.selectedModel ?? null,
getHfDatasetName(config),
false,
+ config.projectName || "",
);
runtimeStore.setStarting(true);
@@ -152,7 +153,12 @@ export function useTrainingActions() {
// Re-read config after potential store updates from dataset check
const payload = buildTrainingStartPayload(useTrainingConfigStore.getState());
- runtimeStore.setStartResources(payload.model_name, payload.hf_dataset, false);
+ runtimeStore.setStartResources(
+ payload.model_name,
+ payload.hf_dataset,
+ false,
+ payload.project_name ?? "",
+ );
const response = await startTraining(payload);
if (response.status === "error") {
@@ -196,7 +202,7 @@ export function useTrainingActions() {
const resumeTrainingRunFromHistory = useCallback(async (runId: string): Promise => {
const runtimeStore = useTrainingRuntimeStore.getState();
runtimeStore.setStartError(null);
- runtimeStore.setStartResources(null, null, true);
+ runtimeStore.setStartResources(null, null, true, null);
runtimeStore.setStarting(true);
try {
@@ -220,7 +226,12 @@ export function useTrainingActions() {
resume_from_checkpoint: outputDir,
} as TrainingStartRequest;
- runtimeStore.setStartResources(payload.model_name, payload.hf_dataset, true);
+ runtimeStore.setStartResources(
+ payload.model_name,
+ payload.hf_dataset,
+ true,
+ payload.project_name ?? "",
+ );
// Resume goes straight to startTraining, so it runs the same consent gate as a
// fresh start; otherwise a resumed custom-code run hits the worker block with no dialog.
diff --git a/studio/frontend/src/features/training/index.ts b/studio/frontend/src/features/training/index.ts
index 5157d89582..553dcc2af5 100644
--- a/studio/frontend/src/features/training/index.ts
+++ b/studio/frontend/src/features/training/index.ts
@@ -7,6 +7,11 @@ export {
useTrainingRuntimeStore,
} from "./stores/training-runtime-store";
export { useTrainingActions } from "./hooks/use-training-actions";
+
+export {
+ getTrainingRunDisplayTitle,
+ getTrainingRunModelSubtitle,
+} from "./lib/run-display";
export { useTrainingHistorySidebarItems } from "./hooks/use-training-history-sidebar";
export { useTrainingRuntimeLifecycle } from "./hooks/use-training-runtime-lifecycle";
export { useTrainingCompletionWatch } from "./hooks/use-training-completion-watch";
diff --git a/studio/frontend/src/features/training/lib/run-display.ts b/studio/frontend/src/features/training/lib/run-display.ts
new file mode 100644
index 0000000000..b691b257c4
--- /dev/null
+++ b/studio/frontend/src/features/training/lib/run-display.ts
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
+
+import type { TrainingRunSummary } from "../types/history";
+
+type TrainingRunTitleFields = Pick<
+ TrainingRunSummary,
+ "display_name" | "project_name" | "model_name"
+>;
+
+function nonEmpty(value: string | null | undefined): string | null {
+ const trimmed = value?.trim();
+ return trimmed ? trimmed : null;
+}
+
+export function getTrainingRunDisplayTitle(run: TrainingRunTitleFields): string {
+ return nonEmpty(run.display_name) ?? nonEmpty(run.project_name) ?? run.model_name;
+}
+
+export function getTrainingRunModelSubtitle(
+ run: TrainingRunTitleFields,
+): string | null {
+ return getTrainingRunDisplayTitle(run) === run.model_name ? null : run.model_name;
+}
diff --git a/studio/frontend/src/features/training/stores/training-config-store.ts b/studio/frontend/src/features/training/stores/training-config-store.ts
index 85ce25aa97..07be8a247b 100644
--- a/studio/frontend/src/features/training/stores/training-config-store.ts
+++ b/studio/frontend/src/features/training/stores/training-config-store.ts
@@ -58,6 +58,7 @@ const initialState: TrainingConfigState = {
currentStep: MIN_STEP,
modelType: null,
selectedModel: null,
+ projectName: "",
trainingMethod: "qlora",
hfToken: "",
datasetSource: "huggingface",
@@ -613,6 +614,7 @@ export const useTrainingConfigStore = create()(
if (state.modelDefaultsAppliedFor === state.selectedModel) return;
void loadAndApplyModelDefaults(state.selectedModel);
},
+ setProjectName: (projectName) => set({ projectName }),
setTrainingMethod: (trainingMethod) => {
const state = get();
set(
diff --git a/studio/frontend/src/features/training/stores/training-runtime-store.ts b/studio/frontend/src/features/training/stores/training-runtime-store.ts
index 9eaaa98c0e..acc80a3be2 100644
--- a/studio/frontend/src/features/training/stores/training-runtime-store.ts
+++ b/studio/frontend/src/features/training/stores/training-runtime-store.ts
@@ -24,6 +24,7 @@ const initialState: TrainingRuntimeState = {
startError: null,
startModelName: null,
startDatasetName: null,
+ startProjectName: null,
startFromResume: false,
sseConnected: false,
firstStepReceived: false,
@@ -125,8 +126,12 @@ export const useTrainingRuntimeStore = create()((set) => (
setHasHydrated: (value) => set({ hasHydrated: value }),
setStarting: (value) => set({ isStarting: value }),
setStartError: (value) => set({ startError: value }),
- setStartResources: (startModelName, startDatasetName, startFromResume = false) =>
- set({ startModelName, startDatasetName, startFromResume }),
+ setStartResources: (
+ startModelName,
+ startDatasetName,
+ startFromResume = false,
+ startProjectName = null,
+ ) => set({ startModelName, startDatasetName, startProjectName, startFromResume }),
setSseConnected: (value) => set({ sseConnected: value }),
setLastEventId: (value) => set({ lastEventId: value }),
diff --git a/studio/frontend/src/features/training/types/api.ts b/studio/frontend/src/features/training/types/api.ts
index 4f7a41bdea..ecd29a2ff0 100644
--- a/studio/frontend/src/features/training/types/api.ts
+++ b/studio/frontend/src/features/training/types/api.ts
@@ -5,6 +5,7 @@ import type { S3Config } from "@/types/training";
export interface TrainingStartRequest {
model_name: string;
+ project_name: string | null;
training_type: string;
hf_token: string | null;
load_in_4bit: boolean;
diff --git a/studio/frontend/src/features/training/types/config.ts b/studio/frontend/src/features/training/types/config.ts
index d24ce31a6a..5658dfc7a1 100644
--- a/studio/frontend/src/features/training/types/config.ts
+++ b/studio/frontend/src/features/training/types/config.ts
@@ -21,6 +21,7 @@ export interface TrainingConfigState {
currentStep: StepNumber;
modelType: ModelType | null;
selectedModel: string | null;
+ projectName: string;
trainingMethod: TrainingMethod;
hfToken: string;
datasetSource: DatasetSource;
@@ -95,6 +96,7 @@ export interface TrainingConfigActions {
prevStep: () => void;
setModelType: (type: ModelType) => void;
setSelectedModel: (model: string | null) => void;
+ setProjectName: (value: string) => void;
ensureModelDefaultsLoaded: () => void;
ensureDatasetChecked: () => void;
setTrainingMethod: (method: TrainingMethod) => void;
diff --git a/studio/frontend/src/features/training/types/history.ts b/studio/frontend/src/features/training/types/history.ts
index 45d83a31d2..99e9bdcb17 100644
--- a/studio/frontend/src/features/training/types/history.ts
+++ b/studio/frontend/src/features/training/types/history.ts
@@ -5,6 +5,7 @@ export interface TrainingRunSummary {
id: string;
status: "running" | "completed" | "stopped" | "error";
model_name: string;
+ project_name: string | null;
dataset_name: string;
display_name: string | null;
started_at: string;
diff --git a/studio/frontend/src/features/training/types/runtime.ts b/studio/frontend/src/features/training/types/runtime.ts
index c27a2f2bed..8ed0ce0037 100644
--- a/studio/frontend/src/features/training/types/runtime.ts
+++ b/studio/frontend/src/features/training/types/runtime.ts
@@ -83,6 +83,7 @@ export interface TrainingRuntimeState {
startError: string | null;
startModelName: string | null;
startDatasetName: string | null;
+ startProjectName: string | null;
startFromResume: boolean;
sseConnected: boolean;
firstStepReceived: boolean;
@@ -121,6 +122,7 @@ export interface TrainingRuntimeActions {
modelName: string | null,
datasetName: string | null,
fromResume?: boolean,
+ projectName?: string | null,
) => void;
setSseConnected: (value: boolean) => void;
setLastEventId: (value: number | null) => void;
@@ -160,6 +162,7 @@ export interface TrainingViewData {
// Config summary
modelName: string;
+ projectName: string | null;
trainingMethod: string;
// Time-series (for ChartsSection)
diff --git a/studio/frontend/src/i18n/locales/en.ts b/studio/frontend/src/i18n/locales/en.ts
index 019a23a891..f3c9eef44e 100644
--- a/studio/frontend/src/i18n/locales/en.ts
+++ b/studio/frontend/src/i18n/locales/en.ts
@@ -609,6 +609,10 @@ export const en = {
params: {
title: "Parameters",
description: "Configure training hyperparameters",
+ projectName: "Project Name",
+ optional: "Optional",
+ projectNameDescription:
+ "Used in training output folder names, export defaults, and history.",
loraSettings: "LoRA Settings",
trainingHyperparameters: "Training Hyperparameters",
maxSteps: "Max Steps",
@@ -850,6 +854,7 @@ export const en = {
loss: "Loss",
lr: "LR",
gradNorm: "Grad Norm",
+ project: "Project",
model: "Model",
method: "Method",
elapsed: "Elapsed: {value}",
diff --git a/studio/frontend/src/i18n/locales/zh-CN.ts b/studio/frontend/src/i18n/locales/zh-CN.ts
index bc239294bd..5fd31dc7cb 100644
--- a/studio/frontend/src/i18n/locales/zh-CN.ts
+++ b/studio/frontend/src/i18n/locales/zh-CN.ts
@@ -541,6 +541,9 @@ export const zhCN = {
params: {
title: "参数",
description: "配置训练超参数",
+ projectName: "项目名称",
+ optional: "可选",
+ projectNameDescription: "用于训练输出文件夹名称、导出默认值和历史记录。",
loraSettings: "LoRA 设置",
trainingHyperparameters: "训练超参数",
maxSteps: "最大步数",
@@ -766,6 +769,7 @@ export const zhCN = {
loss: "Loss",
lr: "LR",
gradNorm: "梯度范数",
+ project: "项目",
model: "模型",
method: "方法",
elapsed: "已用时间:{value}",